Compare commits

...

12 Commits

Author SHA1 Message Date
nino-porcino 7dec136a98 Delete test_066_3003.rar 2022-04-04 09:52:53 +02:00
nino-porcino 823f21ea26 initialize VIA in LOAD and ASAVE for applesoft basic 2022-04-04 09:52:04 +02:00
nino-porcino 6a8d18e89d add "not a file" check 2022-04-03 12:56:56 +02:00
nino-porcino 51bd39ac8c rearrange input buffer and lowram layout to allow longer file names 2022-04-02 10:10:13 +02:00
nino-porcino c795bc7946 update command table 2022-04-02 10:09:33 +02:00
nino-porcino cfce31a8b0 fix bug 2022-04-02 10:09:24 +02:00
nino-porcino 6269887461 Update README.md 2022-03-30 11:57:03 +02:00
nino-porcino cf126ede50 ASAVE, LOAD #F8, remove JMP, 0000R syntax 2022-03-30 11:42:53 +02:00
nino-porcino 9e0651061c format F8 and unknown tagged file names 2022-03-30 11:41:54 +02:00
nino-porcino ae4f61cf28 add viaclock demo 2022-03-28 19:26:46 +02:00
nino-porcino 7852ff37c2 sd card command MOUNT, SD OS v1.1 2022-03-28 19:26:30 +02:00
nino-porcino a2105f88c6 remove loading dots 2022-03-28 17:50:48 +02:00
14 changed files with 295 additions and 80 deletions

View File

@ -12,12 +12,14 @@ indicates the file type (two characters) and the hex loading address (4 characte
`#06` for plain binary files
`#F1` for BASIC programs
`#F8` for Applesoft BASIC lite programs
E.g.:
`BASIC#06E000` is binary file named `BASIC` that loads at address `$E000`.
`STARTREK#F10300` is a BASIC program named `STARTREK` that loads at address `$0300`.
`LEMO#F80801` is a AppleSoft BASIC program named `LEMO` that loads at address `$0801`.
Tagged file names are used by the `LOAD`, `RUN`, `SAVE` and `DIR` commands to simplify working with files. For example to execute the above files, it's enough to type:
Tagged file names are used by the `LOAD`, `RUN`, `SAVE`, `ASAVE` and `DIR` commands to simplify working with files. For example to execute the above files, it's enough to type:
```
LOAD BASIC
@ -51,6 +53,10 @@ RUN STARTREK
Saves a file to the SD card. If `start` and `end` are specified, a binary file with tag `#06` will be created with the memory content from the address range `start`-`end` (included).
If `start` and `end` are not specified, the BASIC program currently loaded in memory will be created with the corresponding `#F1` tag.
`ASAVE filename`
Saves a AppleSoft BASIC lite file to the SD card. The program currently loaded in memory will be created with the corresponding `#F8` tag.
`RUN filename`
Same as `LOAD` but runs the file after loading it. Binary files are exectuted at the starting address specified in the file name tag; BASIC files are `RUN` from the BASIC interpreter.
@ -87,9 +93,14 @@ RUN STARTREK
Prints `LOMEM` and `HIMEM` pointers from the BASIC program currently loaded in memory.
`JMP address`
`address R`
Makes the CPU jump at the specified address.
Runs the program loaded at the specified memory address. Useful addresses:
`6000R` AppleSoft BASIC cold start (needed at least once)
`6003R` AppleSoft BASIC warm start (do not destroy the BASIC program in RAM)
`E000R` Integer BASIC cold start
`EFECR` Integer BASIC "RUN" command (can be used as a warm entry point)
`8000R` SD card OS command prompt
`TIME value`

View File

@ -36,6 +36,7 @@ in Verilog syntax: data = { PORTB[1:0], PORTD[7:2] };
*/
// FASTWRITE not working (yet)
// #define FASTWRITE 1
#ifdef FASTWRITE
@ -46,9 +47,6 @@ in Verilog syntax: data = { PORTB[1:0], PORTD[7:2] };
#define set_mcu_strobe(c) digitalWrite(MCU_STROBE,(c))
#endif
// #include <Regexp.h>
#include <SPI.h>
#include "SdFat.h"
@ -218,6 +216,7 @@ const byte CMD_MKDIR = 14;
const byte CMD_PWD = 19;
const byte CMD_RMDIR = 15;
const byte CMD_TEST = 20;
const byte CMD_MOUNT = 23;
const byte ERR_RESPONSE = 255;
const byte WAIT_RESPONSE = 1;
@ -229,6 +228,7 @@ char tmp[32];
char cd_path[64];
// fixed messages
const char *NOT_A_FILE = "?NOT A FILE";
const char *FILE_NOT_FOUND = "?FILE NOT FOUND";
const char *CANT_OPEN_FILE = "?CAN'T OPEN FILE";
const char *ALREADY_EXISTS = "?FILE ALREADY EXISTS";
@ -244,6 +244,8 @@ const char *CANT_MAKE_DIR = "?CAN'T MAKE DIR";
const char *DIR_CREATED = " (DIR) CREATED";
const char *CANT_CD_DIR = "?CAN'T CHANGE DIR";
const char *NOT_A_DIRECTORY = "?NOT A DIRECTORY";
const char *MOUNT_OK = "MOUNTING SDCARD...\rOK";
const char *MOUNT_FAILED = "?SD CARD ERROR";
// file structures used to operate with the SD card
File myFile;
@ -254,9 +256,7 @@ void setup() {
Serial.begin(9600);
Serial.println(F("SDCARD library: SDFat.h"));
// initialize SD card
if (!SD.begin(SD_CS_PIN)) Serial.println(F("SD card initialization failed"));
else Serial.println(F("SD card initialized"));
mount_sdcard();
// control pins setup
pinMode(CPU_STROBE, INPUT);
@ -267,21 +267,21 @@ void setup() {
last_dir = -1; // no previous data direction
set_data_port_direction(DIR_INPUT);
/*
// regex disabled for now
MatchState ms;
char buf [100] = { "The quick " };
ms.Target (buf);
char result = ms.Match ("f.x");
if(result >0) {
Serial.println("match!");
}
*/
// set working directory to root
strcpy(cd_path, "/");
}
bool mount_sdcard() {
// initialize SD card
if(!SD.begin(SD_CS_PIN)) {
Serial.println(F("SD card initialization failed"));
return false;
}
else {
Serial.println(F("SD card initialized"));
return true;
}
}
// **************************************************************************************
// **************************************************************************************
@ -419,8 +419,15 @@ void send_directory_entry(byte command) {
strcpy(type,"BAS ");
strcpy(address,x+2);
}
else if(x[0]=='F' && x[1]=='8') {
strcpy(type,"ASB ");
strcpy(address,x+2);
}
else {
strcpy(type,"??? ");
type[0] = '#';
type[1] = x[0];
type[2] = x[1];
strcpy(address,x+2);
}
}
@ -476,7 +483,16 @@ void comando_read() {
send_byte_to_cpu(ERR_RESPONSE);
send_string_to_cpu(CANT_OPEN_FILE);
return;
}
}
if(myFile.isDirectory()) {
myFile.close();
Serial.println(F("not a file"));
send_byte_to_cpu(ERR_RESPONSE);
send_string_to_cpu(NOT_A_FILE);
return;
}
Serial.println(F("file opened on the SD card"));
// ok response
@ -648,7 +664,16 @@ void comando_load() {
send_byte_to_cpu(ERR_RESPONSE);
send_string_to_cpu(CANT_OPEN_FILE);
return;
}
}
if(myFile.isDirectory()) {
myFile.close();
Serial.println(F("not a file"));
send_byte_to_cpu(ERR_RESPONSE);
send_string_to_cpu(NOT_A_FILE);
return;
}
Serial.println(F("file opened on the SD card"));
// ok response
@ -775,11 +800,11 @@ byte matchname(char *filename, char *dest) {
Serial.println(dest);
// verify the match
int len = strlen(filename);
int len = strlen(file_name);
bool match_found = strncmp(file_name, dest, len) == 0;
// enforce exact match
if(scanmode == 0) match_found = match_found && dest[strlen(filename)] == '#';
if(scanmode == 0) match_found = match_found && dest[strlen(file_name)] == '#';
if(match_found) {
if(scanmode==0) Serial.println(F("found an exact match"));
@ -794,6 +819,9 @@ byte matchname(char *filename, char *dest) {
strcpy(filename, dest);
if(file_path[0]=='/' && file_path[1]==0) sprintf(dest,"/%s", filename); // case of root folder
else sprintf(dest,"%s/%s", file_path, filename); // case of normal nested folder
//Serial.println(filename);
//Serial.println(file_path);
//Serial.println(dest);
}
myDir.close();
return 1;
@ -1067,6 +1095,11 @@ void loop() {
send_byte_to_cpu(data ^ 0xFF);
}
}
else if(data == CMD_MOUNT) {
bool mount = mount_sdcard();
if(mount) send_string_to_cpu(MOUNT_OK);
else send_string_to_cpu(MOUNT_FAILED);
}
else {
Serial.print(F("unknown command "));
Serial.print(data);

23
demos/sdcard/cmd_asave.h Normal file
View File

@ -0,0 +1,23 @@
// AppleSoft BASIC lite definitions
word const *TXTTAB = (word *) 0x0067;
word const *VARTAB = (word *) 0x0069;
word const *PRGEND = (word *) 0x00AF;
void comando_asave() {
VIA_init(); // needed when called from Applesoft basic after a RESET
woz_puts("SAVING\r");
// appends #F8 + start address (normally: "0801")
tmpword = (word) *TXTTAB;
strcat(filename, "#F8");
append_hex_tmpword(filename);
// launches a normal file write from start_address to end_address
start_address = (word) *TXTTAB;
end_address = (word) *PRGEND;
end_address--;
comando_write();
}

View File

@ -16,12 +16,12 @@ void comando_help() {
}
}
// builds the name of the help file
strcpy((char *)KEYBUFSTART, "/HELP/");
strcat((char *)KEYBUFSTART, filename);
strcat((char *)KEYBUFSTART, ".TXT");
// builds the name of the help file using the input buffer as buffer
strcpy(KEYBUF, "/HELP/");
strcat(KEYBUF, filename);
strcat(KEYBUF, ".TXT");
strcpy(filename, (char *)KEYBUFSTART);
strcpy(filename, KEYBUF);
comando_type();
}

View File

@ -7,6 +7,7 @@
// global cmd
void comando_load_bas() {
VIA_init(); // needed when called from Applesoft basic after a RESET
// send command byte
send_byte_to_MCU(CMD_LOAD);
@ -41,13 +42,14 @@ void comando_load_bas() {
if(*token_ptr == '#') {
if(token_ptr[1] == '0' && token_ptr[2] == '6') { filetype = 0x06; break; }
if(token_ptr[1] == 'F' && token_ptr[2] == '1') { filetype = 0xF1; break; }
if(token_ptr[1] == 'F' && token_ptr[2] == '8') { filetype = 0xF8; break; }
}
if(*token_ptr == 0) break;
token_ptr++;
}
// calculate start address for 0x06 binary file
if(filetype == 0x06) {
if(filetype == 0x06 || filetype == 0xF8) {
token_ptr+=2;
hex_to_word(token_ptr);
start_address = tmpword;
@ -57,7 +59,7 @@ void comando_load_bas() {
receive_word_from_mcu();
if(TIMEOUT) return;
if(filetype != 0x06 && filetype != 0xF1) {
if(filetype == 0) {
woz_puts("?INVALID FILE NAME TAG #");
for(word t=0;t!=tmpword;t++) receive_byte_from_MCU(); // empty buffer
return;
@ -74,10 +76,6 @@ void comando_load_bas() {
byte data = receive_byte_from_MCU();
if(TIMEOUT) return;
*token_ptr++ = data;
#ifdef LOADING_DOTS
if(((byte)t) == 0) woz_putc('.');
#endif
}
// decrease by one for display result
@ -107,6 +105,49 @@ void comando_load_bas() {
return;
}
if(filetype == 0xF8) {
// 0xF8 APPLESOFT BASIC LITE
*TXTTAB = start_address;
// get file bytes
token_ptr = (byte *) start_address;
for(word t=0;t!=tmpword;t++) {
byte data = receive_byte_from_MCU();
if(TIMEOUT) return;
*token_ptr++ = data;
}
*VARTAB = (word) token_ptr;
*PRGEND = (word) token_ptr;
// decrease by one for display result
token_ptr--;
// print feedback to user
woz_putc('\r');
woz_puts(filename);
woz_puts("\r$");
woz_print_hexword(start_address);
woz_puts("-$");
woz_print_hexword((word)token_ptr);
woz_puts(" (");
utoa(tmpword, filename, 10); // use filename as string buffer
woz_puts(filename);
woz_puts(" BYTES)\rOK");
// executes machine language program at start address
//if(cmd == CMD_RUN) {
// woz_putc('\r');
// tmpword = start_address;
// asm {
// jmp (tmpword)
// }
//}
return;
}
// 0xF1 BASIC FILE TYPE
// get file bytes
@ -142,9 +183,6 @@ void comando_load_bas() {
}
token_ptr++;
#ifdef LOADING_DOTS
if(((byte)t) == 0) woz_putc('.');
#endif
}
bas_file_info();

8
demos/sdcard/cmd_mount.h Normal file
View File

@ -0,0 +1,8 @@
void comando_mount() {
// send command byte
send_byte_to_MCU(CMD_MOUNT);
if(TIMEOUT) return;
print_string_response();
return;
}

View File

@ -38,10 +38,6 @@ void comando_read() {
byte data = receive_byte_from_MCU();
if(TIMEOUT) return;
*token_ptr++ = data;
#ifdef LOADING_DOTS
if(((byte)t) == 0) woz_putc('.');
#endif
}
// decrease by one for display result

View File

@ -66,10 +66,6 @@ void comando_save_bas() {
for(token_ptr=*BASIC_LOMEM; token_ptr<(byte *)tmpword; token_ptr++) {
send_byte_to_MCU(*token_ptr);
if(TIMEOUT) return;
#ifdef LOADING_DOTS
if(((byte)token_ptr) == 0) woz_putc('.');
#endif
}
// get second response

View File

@ -42,10 +42,6 @@ void comando_write() {
for(word t=0;t<tmpword;t++) {
send_byte_to_MCU(*token_ptr++);
if(TIMEOUT) return;
#ifdef LOADING_DOTS
if(((byte)t) == 0) woz_putc('.');
#endif
}
// get second response

View File

@ -3,17 +3,18 @@
byte **const BASIC_LOMEM = (byte **) 0x004a; // lomem pointer used by integer BASIC
byte **const BASIC_HIMEM = (byte **) 0x004c; // himem pointer used by integer BASIC
byte *const KEYBUF = (byte *) 0x0200; // use the same keyboard buffer as in WOZ monitor
#define KEYBUFSTART (0x200)
#define KEYBUFLEN (79)
// keyboard buffer 0x200-27F uses only the first 40 bytes, the rest is recycled for free mem
byte *const command = (byte *) (KEYBUFSTART+KEYBUFLEN ); // [6] stores a 5 character command
byte *const filename = (byte *) (KEYBUFSTART+KEYBUFLEN+6 ); // [33] stores a filename or pattern
byte *const hex1 = (byte *) (KEYBUFSTART+KEYBUFLEN+6+33 ); // [5] stores a hex parameter
byte *const hex2 = (byte *) (KEYBUFSTART+KEYBUFLEN+6+33+5); // [5] stores a hex parameter
byte *const filename = (byte *) (KEYBUFSTART ); // $0200-$021F [33] stores a filename or pattern
byte *const KEYBUF = (byte *) 0x0220; // $0220-$026F [80] keyboard input
byte *const hex1 = (byte *) (KEYBUFSTART+33+KEYBUFLEN ); // $0270-$0274 [5] stores a hex parameter
byte *const hex2 = (byte *) (KEYBUFSTART+33+KEYBUFLEN+5 ); // $0275-$0279 [5] stores a hex parameter
byte *const command = (byte *) (KEYBUFSTART+33+KEYBUFLEN+5+5); // $027A-$027F [6] stores a 5 character command
const byte OK_RESPONSE = 0x00;
const byte WAIT_RESPONSE = 0x01;
@ -29,8 +30,8 @@ const byte CMD_RUN = 5;
const byte CMD_SAVE = 6;
const byte CMD_TYPE = 7;
const byte CMD_DUMP = 8;
const byte CMD_JMP = 9;
const byte CMD_BAS = 10;
const byte CMD_ASAVE = 9;
const byte CMD_BAS = 10;
const byte CMD_DEL = 11;
const byte CMD_LS = 12;
const byte CMD_CD = 13;
@ -43,7 +44,8 @@ const byte CMD_PWD = 19;
const byte CMD_TEST = 20;
const byte CMD_HELP = 21;
const byte CMD_QMARK = 22;
const byte CMD_EXIT = 23;
const byte CMD_MOUNT = 23;
const byte CMD_EXIT = 25;
// the list of recognized commands
byte *DOS_COMMANDS[] = {
@ -56,7 +58,7 @@ byte *DOS_COMMANDS[] = {
"SAVE",
"TYPE",
"DUMP",
"JMP",
"ASAVE",
"BAS",
"DEL",
"LS",
@ -70,12 +72,13 @@ byte *DOS_COMMANDS[] = {
"TEST",
"HELP",
"?",
"MOUNT",
"EXIT"
};
// chesum table
// checksum table
byte chksum_table[] = {
0xa7,0xe9,0xf8,0xef,0xeb,0xfe,0xef,0xee,0x8a,0xe8,0xf3,0xa7,0xa7,0xeb,0xe4,0xfe,0xe5,0xe4,0xe3,0xe4,0xe5,0x8a,0xfa,0xe5,0xf8,0xe9,0xe3,0xe4,0xe5,0x8a,0x8a,0x8a,0x82,0xf9,0xe5,0xec,0xfe,0xfd,0xeb,0xf8,0xef,0x83,0xa7,0xe9,0xe6,0xeb,0xff,0xee,0xe3,0xe5,0x8a,0xfa,0xeb,0xf8,0xe7,0xe3,0xed,0xe3,0xeb,0xe4,0xe3,0x8a,0x82,0xe2,0xeb,0xf8,0xee,0xfd,0xeb,0xf8,0xef,0x83,0xa7,0x00
0x80,0x80,0x80,0x8a,0xf3,0xe5,0xff,0x8a,0xe0,0xff,0xf9,0xfe,0x8a,0xec,0xe5,0xff,0xe4,0xee,0x8a,0xeb,0xe4,0x8a,0xef,0xeb,0xf9,0xfe,0xef,0xf8,0x8a,0xef,0xed,0xed,0x8b,0x8b,0x8a,0x80,0x80,0x80,0xa7,0xa7,0xeb,0xff,0xfe,0xe2,0xe5,0xf8,0xf9,0x90,0xa7,0xa7,0xeb,0xe4,0xfe,0xe5,0xe4,0xe3,0xe4,0xe5,0x8a,0xfa,0xe5,0xf8,0xe9,0xe3,0xe4,0xe5,0x8a,0x8a,0x8a,0x82,0xf9,0xe5,0xec,0xfe,0xfd,0xeb,0xf8,0xef,0x83,0xa7,0xe9,0xe6,0xeb,0xff,0xee,0xe3,0xe5,0x8a,0xfa,0xeb,0xf8,0xe7,0xe3,0xed,0xe3,0xeb,0xe4,0xe3,0x8a,0x82,0xe2,0xeb,0xf8,0xee,0xfd,0xeb,0xf8,0xef,0x83,0xa7,0x00
};
// parse a string, get the first string delimited by space or end of string
@ -165,8 +168,9 @@ void append_hex_tmpword(char *dest) {
#include "cmd_read.h"
#include "cmd_write.h"
#include "cmd_load.h"
#include "cmd_save.h"
#include "cmd_asave.h"
#include "cmd_load.h"
#include "cmd_type.h"
#include "cmd_dump.h"
#include "cmd_del.h"
@ -177,12 +181,13 @@ void append_hex_tmpword(char *dest) {
#include "cmd_pwd.h"
#include "cmd_test.h"
#include "cmd_help.h"
#include "cmd_mount.h"
void console() {
VIA_init();
woz_puts("\r\r*** SD CARD OS 1.0\r\r");
woz_puts("\r\r*** SD CARD OS 1.1\r\r");
cmd = 0;
@ -322,6 +327,14 @@ void console() {
comando_save_bas();
}
}
else if(cmd == CMD_ASAVE) {
get_token(filename, 32); // parse filename
if(filename[0] == 0) {
woz_puts("?MISSING FILENAME");
continue;
}
comando_asave();
}
else if(cmd == CMD_TYPE) {
get_token(filename, 32); // parse filename
if(filename[0] == 0) {
@ -353,17 +366,6 @@ void console() {
}
comando_dump();
}
else if(cmd == CMD_JMP) {
get_token(hex1, 4); // parse hex
hex_to_word(hex1);
if(!hex_to_word_ok) {
woz_puts("?BAD ARGUMENT");
continue;
}
asm {
jmp (tmpword)
}
}
else if(cmd == CMD_BAS) {
woz_puts("BAS ");
bas_info();
@ -406,15 +408,29 @@ void console() {
get_token(filename, 32); // parse filename
comando_help();
}
else if(cmd == CMD_MOUNT) {
comando_mount();
}
else if(cmd == CMD_EXIT) {
woz_puts("BYE\r");
woz_mon();
}
else {
if(strlen(command)!=0) {
byte l = strlen(command);
if(l!=0) {
// attempt to parse XXXXR
if(command[l-1] == 'R') {
command[l-1] = 0;
hex_to_word(command);
if(hex_to_word_ok) {
asm {
jmp (tmpword)
}
}
}
woz_puts(command);
woz_puts("??");
}
}
}
if(TIMEOUT) {

View File

@ -197,8 +197,6 @@ void send_word_to_mcu() {
send_byte_to_MCU( *((byte *)(&tmpword+1)) );
}
// #define LOADING_DOTS 0
#include "console.h"
void main() {

View File

@ -1,2 +1,3 @@
@call ..\..\tools\build viatimer
rem @call ..\..\tools\build viatimer
@call ..\..\tools\build viaclock

Binary file not shown.

99
demos/viatimer/viaclock.c Normal file
View File

@ -0,0 +1,99 @@
#define APPLE1_USE_WOZ_MONITOR 1
#define INPUT_LINE_PROMPT_CHAR '?'
#include <string.h>
#include <utils.h>
#include <apple1.h>
#include <via.h>
#include <interrupt.h>
#include <stdlib.h>
#include <division.h>
#include "..\..\lib\c64font.h"
const word ONE_TICK = 16666; // timer constant for 1/60 second
void enable_timer_interrupt() {
// install the interrupt handler
install_interrupt((word) &time_interrupt_handler);
*VIA_IER = 0b11000000; // enable T1 interrupts
*VIA_ACR = 0b01000000; // T1 continous, PB7 disabled
*VIA_T1CL = BYTE0(ONE_TICK); // programs the counter to 1/60
*VIA_T1CH = BYTE1(ONE_TICK); // and starts counting
}
void disable_timer_interrupt() {
*VIA_IER = 0b01000000; // disable T1 interrupts
*VIA_ACR = 0; // stop T1 (makes it one-shot)
}
byte last_min = 0xFF;
void print_digit_scanline(byte c, byte y) {
word index = ((word) c-32) * 8 + (word) y;
byte *ptr = FONT + index;
byte mask = 128;
for(byte t=0;t<8;t++) {
if(*ptr & mask) woz_putc('@');
else woz_putc(' ');
mask = mask >> 1;
}
}
void print_clock() {
byte c1,c2,c3,c4;
if(last_min != _minutes) {
last_min = _minutes;
c1 = divr8u(_hours, 10, 0); c2 = rem8u;
c3 = divr8u(_minutes, 10, 0); c4 = rem8u;
woz_puts("\r\r\r\r\r\r\r\r");
for(byte t=0;t<8;t++) {
print_digit_scanline(c1+'0',t);
print_digit_scanline(c2+'0',t);
print_digit_scanline( ':',t);
print_digit_scanline(c3+'0',t);
print_digit_scanline(c4+'0',t);
}
woz_puts("\r\r\r\r\r\r\r\r");
}
}
void bye() {
disable_timer_interrupt();
woz_puts("BYE!\r");
woz_mon();
}
byte *const KEYBUF = (byte *) 0x0200; // use the same keyboard buffer as in WOZ monitor
void main() {
enable_timer_interrupt();
woz_puts("\r\r*** APPLE1 CLOCK ***\r\r");
woz_puts("\r\rREQUIRES A VIA 6522 AT $A000\r\r");
woz_puts("\rWHAT TIME IS IT (HOURS ) "); apple1_input_line_prompt(KEYBUF, 2);
_hours = (byte) atoi(KEYBUF);
woz_puts("\rWHAT TIME IS IT (MINUTES) "); apple1_input_line_prompt(KEYBUF, 2);
_minutes = (byte) atoi(KEYBUF);
woz_putc('\r');
while(1) {
byte k = apple1_readkey();
if(k=='X') break;
print_clock();
}
bye();
}