概要
前回はSerialクラスを作りましたが今回はSysTickタイマーを使って時間関係の処理を作っていきたいと思います。
SysTickタイマーとは?
CH32Vシリーズでは複数のタイマーを搭載しています。
上記はCH32V103のものですが、4種類のタイマーがあります。
名前 | 内容 |
---|---|
TMI1 | Advanced-control Timer |
TMI2 | General-purpose |
TMI3 | General-purpose |
TMI4 | General-purpose |
IWDG | Independent Watchdog |
WWDG | Window Watchdog |
SysTick | System Time Base Timer |
区分的には上記になっていました。今回利用するのはSysTickになります。このタイマーはどのボードでも1つだけ搭載されている基礎的なタイマーとなります。RTOSなどでタスクの切り替えで利用しているタイマーで、切り替え間隔をTickと呼びます。Tickの間隔はいろいろあるのですがarduino-esp32のFreeRTOSだと1ms間隔になっています。
当初はFreeRTOSを搭載しようとも考えていたのですが、省メモリでシンクルコアだとあまりメリットが無いかもしれないのでまずはRTOSなしのArduino環境を準備しています。
チップ | SysTickの仕様 |
---|---|
CH32V003 | 32Bitのincremental counter |
CH32V006 | 32Bitのincremental counter |
CH32V103 | 64Bitのincremental counter |
CH32V20x | 64Bitのincremental or decremental counter |
CH32V307 | 64Bitのincremental or decremental counter |
CH32X035 | 64Bitのincremental or decremental counter |
CH32L103 | 64Bitのincremental or decremental counter |
チップごとのSysTickタイマーの仕様を調べたところ、上記の区分で3種類ありました。ビット数は精度でどれだけ長いタイマーを作れるのかなのでいいとして、インクリメンタルの増加とデクリメンタルの減少があります。
タイマーは0から始まって既定値まで増えると割り込みがかかるインクリメンタルと、既定値にセットして減らしていって0になると割り込みがかかるデクリメンタルの2種類があります。今回はインクリメンタルで統一したほうが良さそうですね。
EVTのサンプルをnoneOSで動かす
void SYSTICK_Init_Config(u_int64_t ticks)
{
SysTick->CTLR = 0x0000;//購液狼由柴方匂
SysTick->CNTL0 = 0;
SysTick->CNTL1 = 0;
SysTick->CNTL2 = 0;
SysTick->CNTL3 = 0;
SysTick->CNTH0 = 0;
SysTick->CNTH1 = 0;
SysTick->CNTH2 = 0;
SysTick->CNTH3 = 0;
SysTick->CMPLR0 = (u8)(ticks & 0xFF);
SysTick->CMPLR1 = (u8)(ticks >> 8);
SysTick->CMPLR2 = (u8)(ticks >> 16);
SysTick->CMPLR3 = (u8)(ticks >> 24);
SysTick->CMPHR0 = (u8)(ticks >> 32);
SysTick->CMPHR1 = (u8)(ticks >> 40);
SysTick->CMPHR2 = (u8)(ticks >> 48);
SysTick->CMPHR3 = (u8)(ticks >> 56);
NVIC_SetPriority(SysTicK_IRQn, 15);
NVIC_EnableIRQ(SysTicK_IRQn);
SysTick->CTLR = (1<<0);
}
void setup() {
SystemCoreClockUpdate();
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
USART_Printf_Init(115200);
printf("SystemClk:%d\r\n",SystemCoreClock);
printf("ChipID:%08x\r\n", DBGMCU_GetCHIPID());
SYSTICK_Init_Config(SystemCoreClock/8-1);//1s
}
void loop() {
}
uint32_t counter;
#ifdef __cplusplus
extern "C" {
#endif
void SysTick_Handler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
void SysTick_Handler(void)
{
printf("welcome to WCH\r\n");
SysTick->CNTL0 = 0;
SysTick->CNTL1 = 0;
SysTick->CNTL2 = 0;
SysTick->CNTL3 = 0;
SysTick->CNTH0 = 0;
SysTick->CNTH1 = 0;
SysTick->CNTH2 = 0;
SysTick->CNTH3 = 0;
counter++;
printf("Counter:%d\r\n",counter);
}
#ifdef __cplusplus
}
#endif
上記のコードでCH32V103で動かしてみました。SYSTICK_Init_Config関数でSysTickタイマーの設定をすると、割り込みでSysTick_Handler関数が呼ばれるようになります。気をつけないといけないのはextern “C”でC言語から呼び出せる関数にする必要があります。この2つの関数はボードによって違うので個別関数として切り出せばよさそうですね。
void SYSTICK_Init_Config(u_int64_t ticks)
{
SysTick->SR &= ~(1 << 0);//clear State flag
SysTick->CMP = ticks;
SysTick->CNT = 0;
SysTick->CTLR = 0xF;
NVIC_SetPriority(SysTicK_IRQn, 15);
NVIC_EnableIRQ(SysTicK_IRQn);
}
ちなみにCH32V307だと上記のようにかなりシンプルです。CH32V103は64ビットレジスタを8ビットを8つに分割しているので結構面倒な書き方にしています。SRレジスタなどもCH32V103にはなかったので微妙に違うので注意してください。
1msのSysTickタイマーに改造
SYSTICK_Init_Config((SystemCoreClock / 8 / 1000) - 1); //1ms
上記のタイマーセットしている場所を/8で1秒だったので/8/1000で1ミリ秒に変更するだけです。ただこのままだと1ミリ秒にシリアル通信でのprintfが終わらないので少しコードを整理します。
void SYSTICK_Init_Config(u_int64_t ticks) {
SysTick->CTLR = 0x0000;
SysTick->CNTL0 = 0;
SysTick->CNTL1 = 0;
SysTick->CNTL2 = 0;
SysTick->CNTL3 = 0;
SysTick->CNTH0 = 0;
SysTick->CNTH1 = 0;
SysTick->CNTH2 = 0;
SysTick->CNTH3 = 0;
SysTick->CMPLR0 = (u8)(ticks & 0xFF);
SysTick->CMPLR1 = (u8)(ticks >> 8);
SysTick->CMPLR2 = (u8)(ticks >> 16);
SysTick->CMPLR3 = (u8)(ticks >> 24);
SysTick->CMPHR0 = (u8)(ticks >> 32);
SysTick->CMPHR1 = (u8)(ticks >> 40);
SysTick->CMPHR2 = (u8)(ticks >> 48);
SysTick->CMPHR3 = (u8)(ticks >> 56);
NVIC_SetPriority(SysTicK_IRQn, 15);
NVIC_EnableIRQ(SysTicK_IRQn);
SysTick->CTLR = (1 << 0);
}
void setup() {
SystemCoreClockUpdate();
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
USART_Printf_Init(115200);
SYSTICK_Init_Config((SystemCoreClock / 8 / 1000) - 1); //1ms
}
unsigned int counter;
void loop() {
if (counter % 100 == 0) {
printf("counter = %d\n", counter);
}
}
#ifdef __cplusplus
extern "C" {
#endif
void SysTick_Handler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
void SysTick_Handler(void) {
SysTick->CNTL0 = 0;
SysTick->CNTL1 = 0;
SysTick->CNTL2 = 0;
SysTick->CNTL3 = 0;
SysTick->CNTH0 = 0;
SysTick->CNTH1 = 0;
SysTick->CNTH2 = 0;
SysTick->CNTH3 = 0;
counter++;
}
#ifdef __cplusplus
}
#endif
これで1ミリ秒単位で起動してからの経過時間がわかるカウンタができました。この値がmillis関数の戻り値になり、delay関数などの待機時間として利用します。
ただこのままだとまだ足りません。Arduinoではマイクロ秒を返すmicros関数があります。1ミリ秒はSysTickタイマーのカウントでわかるようになりましたが、1ミリ秒未満はこのままだとわかりません。そこでSysTickタイマーの中のカウンタを直接確認するようにします。
インクリメンタルタイマーですので、0からスタートして既定値まで増えていきます。どれだけ増えたのかを確認することで1ミリ秒未満の時間経過もわかります。
ミリ秒未満の値確認
void loop() {
unsigned int cntl = SysTick->CNTL0 + (SysTick->CNTL1 << 8) + (SysTick->CNTL2 << 16) + (SysTick->CNTL3 << 24);
unsigned int cnth = SysTick->CNTH0 + (SysTick->CNTH1 << 8) + (SysTick->CNTH2 << 16) + (SysTick->CNTH3 << 24);
unsigned int cmpl = SysTick->CMPLR0 + (SysTick->CMPLR1 << 8) + (SysTick->CMPLR2 << 16) + (SysTick->CMPLR3 << 24);
unsigned int cmph = SysTick->CMPHR0 + (SysTick->CMPHR1 << 8) + (SysTick->CMPHR2 << 16) + (SysTick->CMPHR3 << 24);
printf("counter = %d, cntl = %d, cnth = %d, cmpl = %d, cmph = %d\n", counter, cntl, cnth, cmpl, cmph);
}
loop関数を変更して、中身のデータを確認してみました。
counter = 91581, cntl = 4833, cnth = 0, cmpl = 8999, cmph = 0
counter = 91587, cntl = 4101, cnth = 0, cmpl = 8999, cmph = 0
counter = 91593, cntl = 3373, cnth = 0, cmpl = 8999, cmph = 0
counter = 91599, cntl = 2640, cnth = 0, cmpl = 8999, cmph = 0
counter = 91605, cntl = 1909, cnth = 0, cmpl = 8999, cmph = 0
counter = 91611, cntl = 1176, cnth = 0, cmpl = 8999, cmph = 0
counter = 91617, cntl = 445, cnth = 0, cmpl = 8999, cmph = 0
counter = 91622, cntl = 7860, cnth = 0, cmpl = 8999, cmph = 0
上記のようなデータになりました。cntlが内部カウンターの下位32ビットで、cnthが上位32ビットです。同じくcmplが対象値の下位32ビットで、cmphが上位32ビットになります。printfは比較的重い処理なので上記の出力をするだけで6ミリ秒程度かかっています。
SYSTICK_Init_Config((SystemCoreClock / 8 / 1000) - 1); //1ms
さて、1ミリ秒未満の値ですが、上記でcmpを設定しているので-1した値が入っています。そのため9000が最大値で、cntの値との割合を確認すれば経過時間がわかりそうです。
void loop() {
unsigned int cnt = SysTick->CNTL0 + (SysTick->CNTL1 << 8) + (SysTick->CNTL2 << 16) + (SysTick->CNTL3 << 24);
unsigned int cmp = SysTick->CMPLR0 + (SysTick->CMPLR1 << 8) + (SysTick->CMPLR2 << 16) + (SysTick->CMPLR3 << 24) + 1;
unsigned int micros = (counter * 1000) + (cnt * 1000 / cmp);
printf("counter = %d, micros = %d, cnt = %d, cmpl = %d\n", counter, micros, cnt, cmp);
}
とりあえず下位32ビットだけ処理をして、cmpは1を足してからmicrosを計算してみます。
counter = 6189, micros = 6189010, cnt = 91, cmpl = 9000
counter = 6194, micros = 6194356, cnt = 3205, cmpl = 9000
counter = 6199, micros = 6199893, cnt = 8039, cmpl = 9000
counter = 6205, micros = 6205429, cnt = 3867, cmpl = 9000
counter = 6210, micros = 6210966, cnt = 8701, cmpl = 9000
counter = 6216, micros = 6216504, cnt = 4536, cmpl = 9000
counter = 6222, micros = 6222040, cnt = 365, cmpl = 9000
counter = 6227, micros = 6227482, cnt = 4338, cmpl = 9000
counter = 6233, micros = 6233018, cnt = 168, cmpl = 9000
出力的にも良さそうですね。
時間巻き戻りチェック
void loop() {
static unsigned int old = 0;
unsigned int cnt = SysTick->CNTL0 + (SysTick->CNTL1 << 8) + (SysTick->CNTL2 << 16) + (SysTick->CNTL3 << 24);
unsigned int cmp = SysTick->CMPLR0 + (SysTick->CMPLR1 << 8) + (SysTick->CMPLR2 << 16) + (SysTick->CMPLR3 << 24) + 1;
unsigned int micros = (counter * 1000) + (cnt * 1000 / cmp);
if (old < micros) {
old = micros;
} else {
printf("counter = %d, old = %d, micros = %d, cnt = %d, cmpl = %d\n", counter, old, micros, cnt, cmp);
}
}
念の為前回の時間を保存しておいて、ちゃんと毎回値が増えているのかを確認してみます。もちろん出力されないのを期待していますが。。。
counter = 3972, old = 3972511, micros = 3972484, cnt = 4363, cmpl = 9000
counter = 3979, old = 3979739, micros = 3979712, cnt = 6411, cmpl = 9000
counter = 3987, old = 3987113, micros = 3987086, cnt = 779, cmpl = 9000
counter = 3993, old = 3993910, micros = 3993883, cnt = 7947, cmpl = 9000
counter = 4001, old = 4001426, micros = 4001399, cnt = 3595, cmpl = 9000
counter = 4009, old = 4009511, micros = 4009484, cnt = 4363, cmpl = 9000
counter = 4016, old = 4016881, micros = 4016854, cnt = 7691, cmpl = 9000
counter = 4025, old = 4025511, micros = 4025484, cnt = 4363, cmpl = 9000
思ったより多くの出力がありました。8ミリ秒間隔ぐらいで時間が巻き戻っていますね、、、
時間巻き戻り調査
void loop() {
const int cnt = 100;
unsigned int data[4][cnt] = {};
for (int i = 0; i < cnt; i++) {
data[0][i] = counter;
data[1][i] = SysTick->CNTL0 + (SysTick->CNTL1 << 8) + (SysTick->CNTL2 << 16) + (SysTick->CNTL3 << 24);
data[2][i] = SysTick->CMPLR0 + (SysTick->CMPLR1 << 8) + (SysTick->CMPLR2 << 16) + (SysTick->CMPLR3 << 24) + 1;
data[3][i] = (data[0][i] * 1000) + (data[1][i] * 1000 / data[2][i]);
}
unsigned int old = 0;
for (int i = 0; i < cnt; i++) {
if (old < data[3][i]) {
old = data[3][i];
} else {
old = data[3][i];
printf("Error\n");
}
printf("counter = %d, micros = %d, cnt = %d, cmpl = %d\n", data[0][i], data[3][i], data[1][i], data[2][i]);
}
}
printfを実行すると遅くなるのでとりあえず100回変数に保存してからゆっくり出力してみます。
counter = 5982, micros = 5982995, cnt = 8962, cmpl = 9000
counter = 5982, micros = 5982996, cnt = 8971, cmpl = 9000
counter = 5982, micros = 5982997, cnt = 8981, cmpl = 9000
counter = 5982, micros = 5982998, cnt = 8990, cmpl = 9000
Error
counter = 5982, micros = 5982000, cnt = 4, cmpl = 9000
counter = 5982, micros = 5982001, cnt = 13, cmpl = 9000
counter = 5982, micros = 5982002, cnt = 23, cmpl = 9000
counter = 5982, micros = 5982003, cnt = 32, cmpl = 9000
発見しました。上記はカウントのリセットは入ってもcounterの値が更新されていないですね。
volatile unsigned int counter;
上記に変更してみました。Arduinoだとvolatileを使っていることが多い気がするのですがマイコンの場合には__IOを使う例が多いです。Arduino Uno R3やESP32はvolatileですが、Arduino UNO R4(renesas_uno)は__IOでした。どっちを指定しても中身は両方volatileになります。
counter = 5973, micros = 5973996, cnt = 8972, cmpl = 9000
counter = 5973, micros = 5973998, cnt = 8982, cmpl = 9000
counter = 5973, micros = 5973999, cnt = 8992, cmpl = 9000
counter = 5974, micros = 5974000, cnt = 6, cmpl = 9000
counter = 5974, micros = 5974001, cnt = 16, cmpl = 9000
counter = 5974, micros = 5974002, cnt = 26, cmpl = 9000
ちゃんと反映するようになりました。これでハッピーかと思いきや、、、
counter = 4323, micros = 4323167, cnt = 1506, cmpl = 9000
counter = 4323, micros = 4323168, cnt = 1516, cmpl = 9000
counter = 4323, micros = 4323169, cnt = 1526, cmpl = 9000
counter = 4323, micros = 4323199, cnt = 1791, cmpl = 9000
Error
counter = 4323, micros = 4323171, cnt = 1545, cmpl = 9000
counter = 4323, micros = 4323172, cnt = 1555, cmpl = 9000
counter = 4323, micros = 4323173, cnt = 1565, cmpl = 9000
カウントが巻き戻っている場所を発見。
void loop() {
const int cnt = 100;
unsigned int data[4][cnt] = {};
for (int i = 0; i < cnt; i++) {
data[0][i] = counter;
data[1][i] = SysTick->CNTL0 + (SysTick->CNTL1 << 8) + (SysTick->CNTL2 << 16) + (SysTick->CNTL3 << 24);
data[2][i] = SysTick->CMPLR0 + (SysTick->CMPLR1 << 8) + (SysTick->CMPLR2 << 16) + (SysTick->CMPLR3 << 24) + 1;
data[3][i] = (data[0][i] * 1000) + (data[1][i] * 1000 / data[2][i]);
}
unsigned int old = 0;
for (int i = 0; i < cnt; i++) {
if (old < data[3][i]) {
old = data[3][i];
} else {
old = data[3][i];
printf("Error\n");
printf("counter = %d, micros = %d, cnt = %d, cmpl = %d\n", data[0][i-2], data[3][i-2], data[1][i-2], data[2][i-2]);
printf("counter = %d, micros = %d, cnt = %d, cmpl = %d\n", data[0][i-1], data[3][i-1], data[1][i-1], data[2][i-1]);
printf("counter = %d, micros = %d, cnt = %d, cmpl = %d\n", data[0][i], data[3][i], data[1][i], data[2][i]);
}
}
}
全体の傾向がわかったのでエラーの場所のみ表示するように変更しました。範囲外を参照する可能性がありますがデバッグなのでとりあえず目をつむります。
Error
counter = 133222, micros = 133222994, cnt = 8950, cmpl = 9000
counter = 133222, micros = 133223023, cnt = 9215, cmpl = 9000
counter = 133222, micros = 133222996, cnt = 8969, cmpl = 9000
9000を超えているのにカウントはリセットされていませんね。16進数にして見てみます。
Error
counter = 3132, micros = 3132198, cnt = 06f6, cmpl = 9000
counter = 3132, micros = 3132227, cnt = 07ff, cmpl = 9000
counter = 3132, micros = 3132200, cnt = 0709, cmpl = 9000
cntのみ16進数にしたところ、上位8ビットのカウントアップの方が早いですね。06から07にカウントアップしてからffから09になっています。
//data[1][i] = SysTick->CNTL0 + (SysTick->CNTL1 << 8) + (SysTick->CNTL2 << 16) + (SysTick->CNTL3 << 24);
data[1][i] = *(uint32_t*)&SysTick->CNTL0;
8ビット単位で取得するとやはり取得タイミングがずれて不整合が起こります。32ビットを一度に取ってくるように修正しました。
修正版
void SYSTICK_Init_Config(u_int64_t ticks) {
SysTick->CTLR = 0x0000;
SysTick->CNTL0 = 0;
SysTick->CNTL1 = 0;
SysTick->CNTL2 = 0;
SysTick->CNTL3 = 0;
SysTick->CNTH0 = 0;
SysTick->CNTH1 = 0;
SysTick->CNTH2 = 0;
SysTick->CNTH3 = 0;
SysTick->CMPLR0 = (u8)(ticks & 0xFF);
SysTick->CMPLR1 = (u8)(ticks >> 8);
SysTick->CMPLR2 = (u8)(ticks >> 16);
SysTick->CMPLR3 = (u8)(ticks >> 24);
SysTick->CMPHR0 = (u8)(ticks >> 32);
SysTick->CMPHR1 = (u8)(ticks >> 40);
SysTick->CMPHR2 = (u8)(ticks >> 48);
SysTick->CMPHR3 = (u8)(ticks >> 56);
NVIC_SetPriority(SysTicK_IRQn, 15);
NVIC_EnableIRQ(SysTicK_IRQn);
SysTick->CTLR = (1 << 0);
}
void setup() {
SystemCoreClockUpdate();
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
USART_Printf_Init(115200);
SYSTICK_Init_Config((SystemCoreClock / 8 / 1000) - 1); //1ms
}
volatile unsigned int _millis;
unsigned long millis(void) {
return _millis;
}
unsigned long _micros(void) {
unsigned int cnt = *(uint32_t*)&SysTick->CNTL0;
unsigned int cmp = *(uint32_t*)&SysTick->CMPLR0 + 1;
unsigned int micros = (millis() * 1000) + (cnt * 1000 / cmp);
return micros;
}
unsigned long micros(void) {
unsigned int micro = _micros();
while ((micro % 1000) < 5) {
micro = _micros();
}
return _micros();
}
void loop() {
unsigned int micro = micros();
static unsigned int old = 0;
static int count = 0;
count++;
if (old < micro) {
old = micro;
if ((count % 1000000) == 0) {
printf("OK count = %d, _millis = %d, old = %d, micro = %d\n", count, millis(), old, micro);
}
} else {
printf("NG count = %d, _millis = %d, old = %d, micro = %d\n", count, millis(), old, micro);
}
}
#ifdef __cplusplus
extern "C" {
#endif
void SysTick_Handler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
void SysTick_Handler(void) {
SysTick->CNTL0 = 0;
SysTick->CNTL1 = 0;
SysTick->CNTL2 = 0;
SysTick->CNTL3 = 0;
SysTick->CNTH0 = 0;
SysTick->CNTH1 = 0;
SysTick->CNTH2 = 0;
SysTick->CNTH3 = 0;
_millis++;
}
#ifdef __cplusplus
}
#endif
いろいろ調整してこんなコードになりました。まずSysTickの読み出しは桁がずれることがあるので8バイト単位で読み込んではいけません。しかし書き込みは8バイト単位で書き込まないと書き込み時のリセットがうまく動かなくなりました。書き込みはEVTのサンプルどおりにしたほうが安全です。
そして巻き戻り対策ですが、下三桁が005以下の場合にはリトライするようなコードにしました。ちょっと無駄なコードなのですがタイミング系は難しいですね。
他のボードにも対応しつつArduino化
他のボードのEVTを参考にしながらどんどん移植をすすめていきます。基本的にCH32V103が一番変わっていて、それ以外はほぼ同じようなコードになりました。CH32V003は少しだけ簡略化されていたり、カウンタが32ビットなので差分があります。
移植自体はすぐに終わりましたが試験しないといけないボードが6種類あるのでその準備だけで結構たいへんです。WCH社のボードは種別の表示が小さいのでチップの特定と、シリアルポートなどもボードごとに違うので細かい確認をする必要があります。
openwchの実装を確認
上記の処理になります。
uint64_t m0 = GetTick();
uint64_t u0 = *((__IO uint32_t *)SYSTICK_CNTH);
u0 = (u0 << 32) + *((__IO uint32_t *)SYSTICK_CNTL);
CH32V103ですが、読み込みは32ビット単位でやっていますね。
if (m1 != m0) {
return (m1 * 1000 + ((tms - u1) * 1000) / tms);
} else {
return (m0 * 1000 + ((tms - u0) * 1000) / tms);
}
値は2回取得してミリ秒が違うときには古い方を採用するようにしているようです。ただミリ秒自体の計算がデクリメンタルのタイマーで実装していますね。。。つまりマイクロ秒はかなりおかしな値が帰ってきていると思います。
CH32V103はインクリメンタルしか設定できないのでレジスタの項目が少ないです。
CH32V307だとMODEのビットが増えてインクリメンタルとデクリメンタルの指定ができるようになっています。
まとめ
1ミリ秒でSysTickの設定をしてタイマー割り込みでカウントアップするところまでは検証していたので、すぐに終わると思ったのですがやっぱりハマりました。
マイクロ秒の部分はどうしてもきれいにいかなかったので力技になってしまいました。割り込み禁止とか排他制御をすればよいのかもしれませんが、まずはこれで進めたいと思います。
コメント