Dropping tonc dependency in LinkUniversal

This commit is contained in:
Rodrigo Alfonso
2024-08-08 08:21:09 -03:00
parent 514c02ccf0
commit fc3e9641e4
9 changed files with 132 additions and 99 deletions

View File

@@ -265,6 +265,7 @@ Name | Type | Default | Description
`gameName` | **const char\*** | `""` | The game name that will be broadcasted in wireless sessions (max `14` characters). The string must be a null-terminated character array. The library uses this to only connect to servers from the same game.
`cableOptions` | **LinkUniversal::CableOptions** | *same as LinkCable* | All the [👾 LinkCable](#-LinkCable) constructor parameters in one *struct*.
`wirelessOptions` | **LinkUniversal::WirelessOptions** | *same as LinkWireless* | All the [📻 LinkWireless](#-LinkWireless) constructor parameters in one *struct*.
`randomSeed` | **int** | `123` | Random seed used for waits to prevent livelocks. If you use _libtonc_, pass `__qran_seed`.
You can also change these compile-time constants:
- `LINK_UNIVERSAL_MAX_PLAYERS`: to set a maximum number of players. The default value is `5`, but since LinkCable's limit is `4`, you might want to decrease it.

View File

@@ -49,8 +49,9 @@ int main() {
// Sender options
if (isSenderMode) {
if (result != LinkCableMultiboot::Result::SUCCESS)
log("LinkCableMultiboot_demo\n (v7.0.0)\n\nPress START to send the "
"ROM...\nPress B to set client mode...");
log("LinkCableMultiboot_demo\n (v7.0.0)\n\n"
"Press START to send the ROM...\n"
"Press B to set client mode...");
if (keys & KEY_START) {
log("Sending... (SELECT to cancel)");

View File

@@ -52,7 +52,8 @@ LinkUniversal* linkUniversal =
.remoteTimeout = LINK_WIRELESS_DEFAULT_REMOTE_TIMEOUT,
.interval = LINK_WIRELESS_DEFAULT_INTERVAL,
.sendTimerId = LINK_WIRELESS_DEFAULT_SEND_TIMER_ID,
.asyncACKTimerId = 0});
.asyncACKTimerId = 0},
__qran_seed);
LinkUniversal* linkConnection = linkUniversal;
#endif

View File

@@ -1,13 +1,13 @@
#include <tonc.h>
#include <string>
#include "../../_lib/interrupt.h"
// BASIC:
// This example sends the pressed buttons to other players.
// (0) Include the header
#include "../../../lib/LinkUniversal.hpp"
#include <tonc.h>
#include <string>
#include "../../_lib/interrupt.h"
void log(std::string text);
void waitFor(u16 key);
@@ -21,11 +21,13 @@ void init() {
int main() {
init();
log("LinkUniversal_basic (v7.0.0)\n\n\nPress A to start\n\n\nhold LEFT on "
"start:\n -> force cable\n\nhold RIGHT on start:\n -> force "
"wireless\n\nhold UP on start:\n -> force wireless server\n\nhold DOWN "
"on start:\n -> force wireless client\n\nhold B on start:\n -> set 2 "
"players (wireless)");
log("LinkUniversal_basic (v7.0.0)\n\n\n"
"Press A to start\n\n\n"
"hold LEFT on start:\n -> force cable\n\n"
"hold RIGHT on start:\n -> force wireless\n\n"
"hold UP on start:\n -> force wireless server\n\n"
"hold DOWN on start:\n -> force wireless client\n\n"
"hold B on start:\n -> set 2 players (wireless)");
waitFor(KEY_A);
u16 initialKeys = ~REG_KEYS & KEY_ANY;
bool forceCable = initialKeys & KEY_LEFT;
@@ -56,7 +58,8 @@ int main() {
.remoteTimeout = LINK_WIRELESS_DEFAULT_REMOTE_TIMEOUT,
.interval = LINK_WIRELESS_DEFAULT_INTERVAL,
.sendTimerId = LINK_WIRELESS_DEFAULT_SEND_TIMER_ID,
.asyncACKTimerId = LINK_WIRELESS_DEFAULT_ASYNC_ACK_TIMER_ID});
.asyncACKTimerId = LINK_WIRELESS_DEFAULT_ASYNC_ACK_TIMER_ID},
__qran_seed);
// (2) Add the required interrupt service routines
interrupt_init();

View File

@@ -150,7 +150,7 @@ void MultibootScene::load() {
log("B: toggle players");
log("UP/DOWN: scroll up/down");
log("L/R: scroll page up/down");
log("UP+L/DOWN+R: scroll to top/bottom");
log("UP+L/DOWN+R: scroll top/bottom");
log("SELECT: clear");
log("---");
log("");

View File

@@ -47,10 +47,12 @@ int main() {
start:
// Options
log("LinkWireless_demo (v7.0.0)\n\n\n\nPress A to start\n\n\n\n\nhold LEFT "
"on start:\n -> disable forwarding\n\nhold UP on start:\n -> disable "
"retransmission\n\nhold B on start:\n -> set 2 players\n\nhold START on "
"start:\n -> async ACK");
log("LinkWireless_demo (v7.0.0)\n\n\n\n"
"Press A to start\n\n\n\n\n"
"hold LEFT on start:\n -> disable forwarding\n\n"
"hold UP on start:\n -> disable retransmission\n\n"
"hold B on start:\n -> set 2 players\n\n"
"hold START on start:\n -> async ACK");
waitFor(KEY_A);
u16 initialKeys = ~REG_KEYS & KEY_ANY;
forwarding = !(initialKeys & KEY_LEFT);

View File

@@ -45,9 +45,8 @@
// (they mean 'disconnected' and 'no data' respectively)
// --------------------------------------------------------------------------
#include <tonc_bios.h>
#include <tonc_core.h>
#include <tonc_math.h>
#include "_link_common.hpp"
#include <cstdio>
#include "LinkCable.hpp"
#include "LinkWireless.hpp"
@@ -58,23 +57,26 @@
// Game ID Filter. Default = 0 (no filter)
#define LINK_UNIVERSAL_GAME_ID_FILTER 0
#define LINK_UNIVERSAL_DISCONNECTED LINK_CABLE_DISCONNECTED
#define LINK_UNIVERSAL_NO_DATA LINK_CABLE_NO_DATA
#define LINK_UNIVERSAL_MAX_ROOM_NUMBER 32000
#define LINK_UNIVERSAL_INIT_WAIT_FRAMES 10
#define LINK_UNIVERSAL_SWITCH_WAIT_FRAMES 25
#define LINK_UNIVERSAL_SWITCH_WAIT_FRAMES_RANDOM 10
#define LINK_UNIVERSAL_BROADCAST_SEARCH_WAIT_FRAMES 10
#define LINK_UNIVERSAL_SERVE_WAIT_FRAMES 60
#define LINK_UNIVERSAL_SERVE_WAIT_FRAMES_RANDOM 30
static volatile char LINK_UNIVERSAL_VERSION[] = "LinkUniversal/v7.0.0";
void LINK_UNIVERSAL_ISR_VBLANK();
void LINK_UNIVERSAL_ISR_SERIAL();
void LINK_UNIVERSAL_ISR_TIMER();
#define LINK_UNIVERSAL_DISCONNECTED LINK_CABLE_DISCONNECTED
#define LINK_UNIVERSAL_NO_DATA LINK_CABLE_NO_DATA
class LinkUniversal {
private:
using u32 = unsigned int;
using u16 = unsigned short;
using u8 = unsigned char;
using s8 = signed char;
static constexpr int MAX_ROOM_NUMBER = 32000;
static constexpr int INIT_WAIT_FRAMES = 10;
static constexpr int SWITCH_WAIT_FRAMES = 25;
static constexpr int SWITCH_WAIT_FRAMES_RANDOM = 10;
static constexpr int BROADCAST_SEARCH_WAIT_FRAMES = 10;
static constexpr int SERVE_WAIT_FRAMES = 60;
static constexpr int SERVE_WAIT_FRAMES_RANDOM = 30;
public:
enum State { INITIALIZING, WAITING, CONNECTED };
enum Mode { LINK_CABLE, LINK_WIRELESS };
@@ -104,25 +106,29 @@ class LinkUniversal {
s8 asyncACKTimerId;
};
explicit LinkUniversal(
Protocol protocol = AUTODETECT,
const char* gameName = "",
CableOptions cableOptions =
CableOptions{
LinkCable::BaudRate::BAUD_RATE_1, LINK_CABLE_DEFAULT_TIMEOUT,
LINK_CABLE_DEFAULT_REMOTE_TIMEOUT, LINK_CABLE_DEFAULT_INTERVAL,
LINK_CABLE_DEFAULT_SEND_TIMER_ID},
WirelessOptions wirelessOptions = WirelessOptions{
true, LINK_UNIVERSAL_MAX_PLAYERS, LINK_WIRELESS_DEFAULT_TIMEOUT,
LINK_WIRELESS_DEFAULT_REMOTE_TIMEOUT, LINK_WIRELESS_DEFAULT_INTERVAL,
LINK_WIRELESS_DEFAULT_SEND_TIMER_ID,
LINK_WIRELESS_DEFAULT_ASYNC_ACK_TIMER_ID}) {
explicit LinkUniversal(Protocol protocol = AUTODETECT,
const char* gameName = "",
CableOptions cableOptions =
CableOptions{LinkCable::BaudRate::BAUD_RATE_1,
LINK_CABLE_DEFAULT_TIMEOUT,
LINK_CABLE_DEFAULT_REMOTE_TIMEOUT,
LINK_CABLE_DEFAULT_INTERVAL,
LINK_CABLE_DEFAULT_SEND_TIMER_ID},
WirelessOptions wirelessOptions =
WirelessOptions{
true, LINK_UNIVERSAL_MAX_PLAYERS,
LINK_WIRELESS_DEFAULT_TIMEOUT,
LINK_WIRELESS_DEFAULT_REMOTE_TIMEOUT,
LINK_WIRELESS_DEFAULT_INTERVAL,
LINK_WIRELESS_DEFAULT_SEND_TIMER_ID,
LINK_WIRELESS_DEFAULT_ASYNC_ACK_TIMER_ID},
int randomSeed = 123) {
this->linkCable = new LinkCable(
cableOptions.baudRate, cableOptions.timeout, cableOptions.remoteTimeout,
cableOptions.interval, cableOptions.sendTimerId);
this->linkWireless = new LinkWireless(
wirelessOptions.retransmission, true,
min(wirelessOptions.maxPlayers, LINK_UNIVERSAL_MAX_PLAYERS),
Link::_min(wirelessOptions.maxPlayers, LINK_UNIVERSAL_MAX_PLAYERS),
wirelessOptions.timeout, wirelessOptions.remoteTimeout,
wirelessOptions.interval, wirelessOptions.sendTimerId,
wirelessOptions.asyncACKTimerId);
@@ -131,7 +137,7 @@ class LinkUniversal {
this->config.gameName = gameName;
}
bool isActive() { return isEnabled; }
[[nodiscard]] bool isActive() { return isEnabled; }
void activate() {
reset();
@@ -146,16 +152,16 @@ class LinkUniversal {
}
void setProtocol(Protocol protocol) { this->config.protocol = protocol; }
Protocol getProtocol() { return this->config.protocol; }
[[nodiscard]] Protocol getProtocol() { return this->config.protocol; }
bool isConnected() { return state == CONNECTED; }
[[nodiscard]] bool isConnected() { return state == CONNECTED; }
u8 playerCount() {
[[nodiscard]] u8 playerCount() {
return mode == LINK_CABLE ? linkCable->playerCount()
: linkWireless->playerCount();
}
u8 currentPlayerId() {
[[nodiscard]] u8 currentPlayerId() {
return mode == LINK_CABLE ? linkCable->currentPlayerId()
: linkWireless->currentPlayerId();
}
@@ -164,10 +170,10 @@ class LinkUniversal {
if (!isEnabled)
return;
u16 keys = ~REG_KEYS & KEY_ANY;
__qran_seed += keys;
__qran_seed += REG_RCNT;
__qran_seed += REG_SIOCNT;
u16 keys = ~Link::_REG_KEYS & Link::_KEY_ANY;
randomSeed += keys;
randomSeed += Link::_REG_RCNT;
randomSeed += Link::_REG_SIOCNT;
if (mode == LINK_CABLE)
linkCable->sync();
@@ -175,7 +181,7 @@ class LinkUniversal {
switch (state) {
case INITIALIZING: {
waitCount++;
if (waitCount > LINK_UNIVERSAL_INIT_WAIT_FRAMES)
if (waitCount > INIT_WAIT_FRAMES)
start();
break;
};
@@ -242,18 +248,22 @@ class LinkUniversal {
: linkWireless->config.sendTimerId;
while (isConnected() && !canRead(playerId) && !cancel()) {
IntrWait(1, IRQ_SERIAL | LINK_CABLE_TIMER_IRQ_IDS[timerId]);
Link::_IntrWait(1, Link::_IRQ_SERIAL | Link::_TIMER_IRQ_IDS[timerId]);
sync();
}
return isConnected() && canRead(playerId);
}
bool canRead(u8 playerId) { return !incomingMessages[playerId].isEmpty(); }
[[nodiscard]] bool canRead(u8 playerId) {
return !incomingMessages[playerId].isEmpty();
}
u16 read(u8 playerId) { return incomingMessages[playerId].pop(); }
u16 peek(u8 playerId) { return incomingMessages[playerId].peek(); }
[[nodiscard]] u16 peek(u8 playerId) {
return incomingMessages[playerId].peek();
}
bool send(u16 data) {
if (data == LINK_CABLE_DISCONNECTED || data == LINK_CABLE_NO_DATA)
@@ -267,17 +277,19 @@ class LinkUniversal {
}
}
State getState() { return state; }
Mode getMode() { return mode; }
LinkWireless::State getWirelessState() { return linkWireless->getState(); }
[[nodiscard]] State getState() { return state; }
[[nodiscard]] Mode getMode() { return mode; }
[[nodiscard]] LinkWireless::State getWirelessState() {
return linkWireless->getState();
}
~LinkUniversal() {
delete linkCable;
delete linkWireless;
}
u32 _getWaitCount() { return waitCount; }
u32 _getSubWaitCount() { return subWaitCount; }
[[nodiscard]] u32 _getWaitCount() { return waitCount; }
[[nodiscard]] u32 _getSubWaitCount() { return subWaitCount; }
void _onVBlank() {
if (mode == LINK_CABLE)
@@ -322,13 +334,16 @@ class LinkUniversal {
u32 switchWait = 0;
u32 subWaitCount = 0;
u32 serveWait = 0;
int randomSeed = 0;
volatile bool isEnabled = false;
void receiveCableMessages() {
int maxPlayers =
min(LINK_CABLE_MAX_PLAYERS,
LINK_UNIVERSAL_MAX_PLAYERS); // TODO: MOVE TO CONSTEXPR
for (u32 i = 0; i < maxPlayers; i++) {
static constexpr u32 MAX_PLAYERS =
LINK_CABLE_MAX_PLAYERS < LINK_UNIVERSAL_MAX_PLAYERS
? LINK_CABLE_MAX_PLAYERS
: LINK_UNIVERSAL_MAX_PLAYERS;
for (u32 i = 0; i < MAX_PLAYERS; i++) {
while (linkCable->canRead(i))
incomingMessages[i].push(linkCable->read(i));
}
@@ -360,7 +375,7 @@ class LinkUniversal {
waitCount = 0;
subWaitCount++;
if (subWaitCount >= LINK_UNIVERSAL_BROADCAST_SEARCH_WAIT_FRAMES) {
if (subWaitCount >= BROADCAST_SEARCH_WAIT_FRAMES) {
if (!tryConnectOrServeWirelessSession())
return false;
}
@@ -407,8 +422,7 @@ class LinkUniversal {
(LINK_UNIVERSAL_GAME_ID_FILTER == 0 ||
server.gameId == LINK_UNIVERSAL_GAME_ID_FILTER)) {
u32 randomNumber = safeStoi(server.userName);
if (randomNumber > maxRandomNumber &&
randomNumber < LINK_UNIVERSAL_MAX_ROOM_NUMBER) {
if (randomNumber > maxRandomNumber && randomNumber < MAX_ROOM_NUMBER) {
maxRandomNumber = randomNumber;
serverIndex = i;
}
@@ -423,9 +437,8 @@ class LinkUniversal {
return false;
subWaitCount = 0;
serveWait = LINK_UNIVERSAL_SERVE_WAIT_FRAMES +
qran_range(1, LINK_UNIVERSAL_SERVE_WAIT_FRAMES_RANDOM);
u32 randomNumber = qran_range(1, LINK_UNIVERSAL_MAX_ROOM_NUMBER);
serveWait = SERVE_WAIT_FRAMES + _qran_range(1, SERVE_WAIT_FRAMES_RANDOM);
u32 randomNumber = _qran_range(1, MAX_ROOM_NUMBER);
char randomNumberStr[6];
std::snprintf(randomNumberStr, sizeof(randomNumberStr), "%d",
randomNumber);
@@ -503,8 +516,7 @@ class LinkUniversal {
void resetState() {
waitCount = 0;
switchWait = LINK_UNIVERSAL_SWITCH_WAIT_FRAMES +
qran_range(1, LINK_UNIVERSAL_SWITCH_WAIT_FRAMES_RANDOM);
switchWait = SWITCH_WAIT_FRAMES + _qran_range(1, SWITCH_WAIT_FRAMES_RANDOM);
subWaitCount = 0;
serveWait = 0;
for (u32 i = 0; i < LINK_UNIVERSAL_MAX_PLAYERS; i++)
@@ -524,6 +536,15 @@ class LinkUniversal {
return num;
}
int _qran(void) {
randomSeed = 1664525 * randomSeed + 1013904223;
return (randomSeed >> 16) & 0x7FFF;
}
int _qran_range(int min, int max) {
return (_qran() * (max - min) >> 15) + min;
}
};
extern LinkUniversal* linkUniversal;

View File

@@ -106,6 +106,7 @@ static volatile char LINK_WIRELESS_VERSION[] = "LinkWireless/v7.0.0";
#define LINK_WIRELESS_MAX_PLAYERS 5
#define LINK_WIRELESS_MIN_PLAYERS 2
#define LINK_WIRELESS_END 0
#define LINK_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH 22
#define LINK_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH 30
#define LINK_WIRELESS_BROADCAST_LENGTH 6
@@ -114,7 +115,9 @@ static volatile char LINK_WIRELESS_VERSION[] = "LinkWireless/v7.0.0";
#define LINK_WIRELESS_MAX_SERVERS \
(LINK_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH / \
LINK_WIRELESS_BROADCAST_RESPONSE_LENGTH)
#define LINK_WIRELESS_END 0
#define LINK_WIRELESS_MAX_GAME_ID 0x7fff
#define LINK_WIRELESS_MAX_GAME_NAME_LENGTH 14
#define LINK_WIRELESS_MAX_USER_NAME_LENGTH 8
#define LINK_WIRELESS_DEFAULT_TIMEOUT 10
#define LINK_WIRELESS_DEFAULT_REMOTE_TIMEOUT 10
#define LINK_WIRELESS_DEFAULT_INTERVAL 50
@@ -153,9 +156,6 @@ class LinkWireless {
static constexpr int TRANSFER_WAIT = 15;
static constexpr int BROADCAST_SEARCH_WAIT_FRAMES = 60;
static constexpr int CMD_TIMEOUT = 100;
static constexpr int MAX_GAME_ID = 0x7fff;
static constexpr int MAX_GAME_NAME_LENGTH = 14;
static constexpr int MAX_USER_NAME_LENGTH = 8;
static constexpr int LOGIN_STEPS = 9;
static constexpr int COMMAND_HEADER_VALUE = 0x9966;
static constexpr int RESPONSE_ACK_VALUE = 0x80;
@@ -233,8 +233,8 @@ class LinkWireless {
struct Server {
u16 id = 0;
u16 gameId;
char gameName[MAX_GAME_NAME_LENGTH + 1];
char userName[MAX_USER_NAME_LENGTH + 1];
char gameName[LINK_WIRELESS_MAX_GAME_NAME_LENGTH + 1];
char userName[LINK_WIRELESS_MAX_USER_NAME_LENGTH + 1];
u8 currentPlayerCount;
bool isFull() { return currentPlayerCount == 0; }
@@ -332,7 +332,7 @@ class LinkWireless {
*/
bool serve(const char* gameName = "",
const char* userName = "",
u16 gameId = MAX_GAME_ID) {
u16 gameId = LINK_WIRELESS_MAX_GAME_ID) {
LINK_WIRELESS_RESET_IF_NEEDED
if (state != AUTHENTICATED && state != SERVING) {
lastError = WRONG_STATE;
@@ -342,25 +342,25 @@ class LinkWireless {
lastError = BUSY_TRY_AGAIN;
return false;
}
if (std::strlen(gameName) > MAX_GAME_NAME_LENGTH) {
if (std::strlen(gameName) > LINK_WIRELESS_MAX_GAME_NAME_LENGTH) {
lastError = GAME_NAME_TOO_LONG;
return false;
}
if (std::strlen(userName) > MAX_GAME_NAME_LENGTH) {
if (std::strlen(userName) > LINK_WIRELESS_MAX_GAME_NAME_LENGTH) {
lastError = USER_NAME_TOO_LONG;
return false;
}
char finalGameName[MAX_GAME_NAME_LENGTH + 1];
char finalUserName[MAX_USER_NAME_LENGTH + 1];
copyName(finalGameName, gameName, MAX_GAME_NAME_LENGTH);
copyName(finalUserName, userName, MAX_USER_NAME_LENGTH);
char finalGameName[LINK_WIRELESS_MAX_GAME_NAME_LENGTH + 1];
char finalUserName[LINK_WIRELESS_MAX_USER_NAME_LENGTH + 1];
copyName(finalGameName, gameName, LINK_WIRELESS_MAX_GAME_NAME_LENGTH);
copyName(finalUserName, userName, LINK_WIRELESS_MAX_USER_NAME_LENGTH);
if (state != SERVING)
setup(config.maxPlayers);
addData(buildU32(buildU16(finalGameName[1], finalGameName[0]),
gameId & MAX_GAME_ID),
gameId & LINK_WIRELESS_MAX_GAME_ID),
true);
addData(buildU32(buildU16(finalGameName[5], finalGameName[4]),
buildU16(finalGameName[3], finalGameName[2])));
@@ -486,7 +486,7 @@ class LinkWireless {
Server server;
server.id = (u16)result.responses[start];
server.gameId = result.responses[start + 1] & MAX_GAME_ID;
server.gameId = result.responses[start + 1] & LINK_WIRELESS_MAX_GAME_ID;
u32 gameI = 0, userI = 0;
recoverName(server.gameName, gameI, result.responses[start + 1], false);
recoverName(server.gameName, gameI, result.responses[start + 2]);

View File

@@ -75,15 +75,19 @@ inline volatile u16* const _REG_SIOMULTI =
inline volatile u16& _REG_VCOUNT =
*reinterpret_cast<volatile u16*>(_REG_BASE + 0x0006);
inline volatile u16& _REG_KEYS =
*reinterpret_cast<volatile u16*>(_REG_BASE + 0x0130);
inline volatile _TMR_REC* const _REG_TM =
reinterpret_cast<volatile _TMR_REC*>(_REG_BASE + 0x0100);
static constexpr u16 _TM_FREQ_1 = 0; // 1 cycle/tick (16.7 MHz)
static constexpr u16 _TM_FREQ_64 = 0x0001; // 64 cycles/tick (262 kHz)
static constexpr u16 _TM_FREQ_256 = 0x0002; // 256 cycles/tick (66 kHz)
static constexpr u16 _TM_FREQ_1024 = 0x0003; // 1024 cycles/tick (16 kHz)
static constexpr u16 _TM_IRQ = 0x0040;
static constexpr u16 _TM_ENABLE = 0x0080;
static constexpr u16 _KEY_ANY = 0x03FF; //!< Here's the Any key :)
static constexpr u16 _TM_FREQ_1 = 0; //!< 1 cycle/tick (16.7 MHz)
static constexpr u16 _TM_FREQ_64 = 0x0001; //!< 64 cycles/tick (262 kHz)
static constexpr u16 _TM_FREQ_256 = 0x0002; //!< 256 cycles/tick (66 kHz)
static constexpr u16 _TM_FREQ_1024 = 0x0003; //!< 1024 cycles/tick (16 kHz)
static constexpr u16 _TM_IRQ = 0x0040; //!< Enable timer irq
static constexpr u16 _TM_ENABLE = 0x0080; //!< Enable timer
static constexpr u16 _IRQ_VBLANK = 0x0001; //!< Catch VBlank irq
static constexpr u16 _IRQ_TIMER0 = 0x0008; //!< Catch timer 0 irq