hehe, I've got a linearly increasing set of POKEs that go up and down from a middle number, something that should be easy to translate over to FOR:NEXT loops but it's working perfectly now, and I really don't want to mess that up. 🙂
Thanks Robin! This was exactly what I was hoping for. Really appreciate the extra time and effort you put in to answering the question as completely as you could!
Hi Robin! Thanks for the video! Many years ago, when I was in school, I made a countdown timer on my TI calculator - to count down the time until our class was over.. It was based on loops, and showed minutes and seconds. It was very imprecise in the beginning, but I kept improving it - when I was bored in class. It got kind of accurate up to one hour!
For the for next method: If the transition to the next power of 2 number is a problem, why not use a range of numbers already in a big power of 2 that won‘t need to transition again for the duration of the loop? Like starting at 1025 instead of 0.
Thank you for pointing that problem out! That's a problem that can occur in many situations even in modern development so it's great to raise awareness of it.
These times are quite a bit different on a PAL machine. Through trial and error I was able to get them pretty close on a PAL machine. Also, just a clarification at 6:46: The Ultimate64 can switch anywhere between 1 and 48Mhz (also between PAL and NTSC on the fly), but the Ultimate 1541 (now called the Ultimate II+) does not have a CPU accelerator in it. 21:31 The Toolkit: BASIC book for the win on understanding that. Might be my new favorite set of books.
I was kind of wondering why he never mentioned that the different clock speeds between PAL and NTSC machines would likely cause an issue with the simple loop timer.
Testing with Vice (C64SC), the loop counts I found were 625 for PAL and 780 for NTSC. I also stashed TI into a variable to eliminate PRINT messing with the count, it doesn't seem to impact the jiffie count though.
Was it only the FOR/NEXT loops that were substantially different on PAL? I would think the TI polling and WAIT methods would be the same, at least within a jiffy. But yes, I should have mentioned PAL. No matter how thorough I think I'm being, and how deep I go down the rabbit hole, there's always more :)
@@8_Bit Again testing with Vice "C64SC" both polling and WAIT works as expected you just have to adjust for 50 jiffies in a second. I think WAIT method is the better choice when you need that level of accuracy, which on the C64 is exceedingly rare.
0:03 I'd like to know how to achieve a sleep(-1). 6:41 You'll be happy to hear that the SuperCPU I recycled several years ago wasn't destroyed but was kept by someone and recently sold to a professor at the University of Windsor. 6:50 The C-128 has a method of boosting the CPU speed also… 7:19 Did they called that the "Double Future" in "Bill & Ted's Excellent Adventure"? 10:37 So it is the extra bit-shifting to align the mantissas for adding that's causing the difference. Your count-limit formula will need a base-2 logarithm in it! 10:56 When X is at 15, it doesn't need to align the values as far to add the 1, but then the result is 16, which will need to be normalized by shifting the mantissa and incrementing the exponent of the result. 11:38 The 32-bit CPUs had a barrel shifter, which can shift any number of bits in linear time. Of course, they also had direct hardware floating-point support. 16:58 Is "5184000" a KERNAL bug? I don't think it should ever show that. It'd be like a clock going from 23:59:59 to 24:00:00 a second later and then going to 0:00:00 the second after that, giving the day an extra second. 20:57 Just because it CAN be done, doesn't mean it SHOULD be done! 28:50 It's just the same bits shifted left one position. 31:12 reminds me of the DankPods credits: ua-cam.com/video/uuS_ZioCfDo/v-deo.html .
That's great your old SuperCPU still exists! It seems like there might be an interesting story behind how you came to know this? That's true that 5184000 seems like it shouldn't exist. Oh, I just did some quick experiments, and if you repeatedly print TI$ quickly when it's about to wrap, then if you're lucky you'll see time "240000" printed once between many "235959" and "000000"! Certainly seems like a bug, but probably only adds 1/60th of a second to the day, instead of a full second. Seems I have to speed up the credits a LOT more to match DankPods. I usually have some reason behind how I customize the credits each episode. This time I (re)discovered a stash of my music that I wanted to use, and when I found this song we deliberately recorded to be only 30 seconds, I thought it would be an amusing contrast in a video all about WAITing to do anything but with the credits.
@@8_Bit: I simply had put my name on my old Commodore equipment and the buyer either was familiar with my name or looked it up, perhaps to see if the items were stolen.
The TI check wraparound is a modulo arithmetic problem. Finding the target threshold is easy; t=TI+d : if t>max t=t-max. The problem is there's time before the wraparound happens, so TI>t already. So you'd wait for TIt. It's a bunch of logic. Furthermore, it doesn't solve the issue of when t is close to max, and we may have no jiffies when TI>t. Checking for TI=t, as you might do in hardware, wouldn't do if basic might be too slow to read TI at the right jiffy (though maybe it is fast enough if you're not printing). The more generic form is comparing (TI-oldTI)%max to delta. Unfortunately there's no modulo operator in the C64 basic, afaik. So you'd calculate dt=(TI-oldTI), then fix it up by adding max if dt
Nice, this is similar to my attempt. I would just simply this formula for clarity: T=X-(5184000-TI)-5 to T=TI+X-(5184000+5) . I guess those 5 jiffies are to compensate for TI changing between the execution of first T calculation and re-calculation. Im not sure if just T=T-5184000 would suffice there.
It was really cool to see the walkthrough of each of these approaches. I immediately thought of the polling TI method, but seeing that bug was interesting. Great video.
Regarding the midnight wrap-around, IBM BASIC had a similar problem when using the TIMER function for delays, and I had an old BASIC reference book that suggested a delay routine that could properly handle the wrapping problem. It looked roughly like this: (TIMER returns current system time in seconds as a float, mod 86400) seconds = (number of seconds to wait) t = TIMER if 86400 - t < seconds + .15 then t = t - 86400: while timer > 0: wend while TIMER < t + seconds: wend It seems like this could be adapted to fit the C64 case. What it seems to do (if I'm interpreting the code correctly) is determine first whether the system timer would overflow during the delay. If not, it delays in pretty much the same way as given in the video. If it will (presumably the .15 in the expression "86400 - t < seconds + .15" is just a safety net to compensate for BASIC execution overhead), then it appears to loop until the system timer has overflowed first and adjusts the value of t so that when it goes to the second while loop (the usual delay loop) it ends up waiting the correct amount of time that remains for the delay *after* the overflow (that is, the total time to wait minus the amount of time it took until the timer overflowed).
my solution to wait using the TI variable and handling the wrap around: 5 TI$ = "235959" 10 PRINT TI 15 S = TI : REM STORE START TIME IN S 20 T = S + 60 : REM STOR END TIME IN T 25 IF T >= 5184000 THEN 50 : REM OVERFL 30 IF TI < T THEN 30 35 GOTO 100 50 T = T - 5184000 : REM WRAP AROUND T 60 IF TI >= S OR TI < T THEN 60 100 PRINT TI
Would a nested loop with outer loop being the number of required seconds and inner being the one second delay. It wouldn't be doing math with big numbers, it might keep it more stable? Thanks for the video Robin, the "directors cut" is making me think more!
Great vid!! I never used the Wait command back in the '80s as a kid programming on my uncle's c64 because I didn't understand what the operands even were. Logically, you'd think it'd be one operand with the delay in milliseconds or jiffies etc. But no! Now thanks to this vid, I understand it basically watches an address for a bit. Ok, how about this: let's suppose you want to make something happen at regular (ish) intervals, but want other code to execute in the meantime? I suggest something like: 50 D=128:D1=5: D=int(D/4): Rem delay in Jiffies, seconds (ish) 100 T=Peek(162)+D:T1=Peek(161)+D1 ... other code... 500 TF = Peek(161): If TF>T1 Then Gosub RegularRoutine 510 if TF = T1 And Peek(162) >T then Gosub RegulaRoutine 520 Goto 500 Perhaps the delay in address 162 is too granular relative to the expected time to complete the ...other code.... bit so that part can be omitted simplifying the setup and checking. As long as there aren't any long loops (significantly longer than the delay vaules) this works kind of like an interrupt routine. In fact, instead of once per loop, there could be multiple checks occuring throughout the ...other code... section if necessary. Might be an interesting topic: how to simulate interrupts in basic? Thanks for the amazing content as always.
I love these videos. A lot of trial and error and multiple solutions to the same problem. This is how I remember getting started with programming. Just "messing" around like this. Great way of learning I think!
Noticing that the TI max value went to 5184000 not 5183999, I tried printed TI$ while wrapping midnight repeatedly and you actually do get "240000" briefly! :-)
Did something similar, just used string to get the time, and then looped till seconds time rolled over to 0, then waited in another string till seconds time rolled over to 2, before exiting. Got me a good 1 minute count routine, which I was using a lot, calling it for delays. Then did some IO, and counted minutes again. Was using this to run some automation to duplicate tapes, outer loop ran the number of total machine cycles, then the next loop ran the machine cycle, for a complete run of the machine, which was variable set at start. Then at the end it stopped, rewound the tape to the beginning ( fixed time, not worth doing anything more complex), and ejected, which also loaded the new tape. Then emulated pressing record and play, and waited for the cycle time again. An update for the wait loop, as I had a lot of time, was to use it to clear the screen every 30 seconds (look in string for seconds equal to 30) and randomly choose a screen location, and print the assorted timers in the system. Glitchy at 30 seconds, so that got a short roughly 1 second for next loop to not spam the screen, and again at the 0 second mark, implementing a simple screen saver for the well used mono HGC monitor I was using for output. Ran for a good number of years with no issue, replacing an original controller that was unreliable and temperamental on it's good days, and downright ornery when it was not. I got tired of fixing this thing, as timings were all set using really long RC delays, using 555 timers and 10000uf capacitors, which was not at all repeatable. Instead looked at the pile of old PC's, and chose a nice reasonably new 386SX16 motherboard that I had 2 spares of, put in the requisite video card, keyboard, 3.5in diskette drive, and MSDOS 6.22. Used a parallel port card as the IO, writing direct to the port address to use the bits to emulate the keys on the wired Matsushita remote control for all functions. Controllers in the case, and powered off the 12V rail, as they had on board regulators, and a bit of software, and the original controller was dismantled. Still got the computer, but no monitor for it, though I still have a keyboard that will work, and it likely will still boot and run.
10 TI$="000000" 20 FOR I=0 TO 50:WAIT 53265,128:NEXT 30 PRINT TI/60 Here I'm counting the frames processed in one second. Alter the value in the FOR statement 50 for PAL, 60 for NSTC. 🙂
@@adymode Thanks. It could be optimized a bit by only adding the day carry when needed. I haven't tested it yet, but I assume an extra test and jump might be quicker than the floating point addition. You can do quite a bit of optimizing when you know the ins and outs of C64 BASIC, but optimized code usually isn't very pretty.
Dave from Dave's Garage had a really great job explaining floating point math. It's the first time I ever heard the word "mantissa." I don't know how I went all this time without knowing that word. Anyway, the funny thing was that the video wasn't even about floating point math. He just covered it to explain something else. I believe it's in the second part of his two parter about "Quake III's Secret Algorithm." I don't know C64 Basic, but in QBasic I always used TIMER, which is just like the TI$, except TIMER counts in milliseconds. Just reference TIMER for your wait loops and you can time things down to almost the millisecond. QBasic code is never quite fast enough to catch the exact millisecond, but you can get within 3 or 4 on a typical 486 of the day.
New lines take longer to process than semicolons, so to get a more accurate reading you should use 10 TI$="000000":FOR I=1 TO 1000:NEXT:?TI This will show the loop to take 63 jiffies instead of 64 when using seperate lines.
Seems like it'd be pretty ugly to use the TOD from BASIC, but I've never tried. The numbers are in BCD, aren't they? Polling them would be pretty bad, and since seconds are separate from the 1/10th of a second, we'd have to read two registers to do accurate timing. Seems like an interesting solution for ML and GEOS programming, but it really doesn't seem appropriate to me for BASIC unless I'm missing something.
@@8_Bit I posted an example in @Shawn Lucas comment "How about reading the TOD registers on the 6526 IC?" This variant is very useful to benchmark routines which inherently run with disabled interrupts ...
There's probably no way to fully fix your wrap-around problem (you're always going to have a single shortened delay time just before midnight), but you can easily avoid the infinite loop by re-arranging your test. This is how I usually do this sort of thing: 20 T=TI 30 IF ABS(TI-T) < 60 then 30 When TI wraps to 0, ABS(TI-T) will become a large positive number, and the loop will terminate.
Those slight divide errors are known as floating point drift and are endemic on machines lacking math coprocessors. This is why computer makers later added math chips.
Without changing the clock: 10 T=(PEEK(162)+60) and 255 20 IF PEEK(162)T THEN 20 (although if there are TSRs running in the background this _could_ fail - but testing under VICE it works reliably in normal circumstances with CPU running anywhere down to 10% of normal).
Although: clc lda 162 adc #60 cmp 162 bne 252 rts in hex: 18 a5 a2 69 3c c5 a2 d0 fc 60 in decimal: 24 165 162 105 60 197 162 208 252 96 compiles down to 10 bytes (arguably you could omit the 'clc' without much error, perhaps it would pause for an extra 1/60 of a second). The code is relocatable, so poke it somewhere available (49152 to 49161) and sys it (sys 49152) when you need a (pretty darn accurate) one second delay.
I think he was trying to get a more definitive method of specifying a certain time, rather than a loop that could have different time lengths depending on CPU load, etc. But I agree, for general purposes the FOR:NEXT loops are definitely the easier method. @@Joel-qz6sd
And finally I understand the WAIT command. The instruction manual I got with my C64 did an absolutely terrible job of explaining this. Commodore did a terrible disservice to their machine by not shipping it with the far more comprehensive user's guide; it might have kept more of its younger users into programming and away from games (that they didn't write themselves).
Busy wait FOR-loops are evil. It's not just about them running faster on faster CPUs, but in compiled code as well. In fact, my BASIC cross compiler detects that these are empty loops and replaces them by setting the variable's value directly to the final value by default...and there goes your wait...:-)
Essentially similar to the final "Another WAIT approach", if you set TI$="000047", then run WAIT 162,64, it should work for one second because it also sets the timer's low byte to 4. POKEing 162 is probably more elegant!
@11:50 that should be interesting! In my experience, no two floating point libraries ever work exactly the same. Had loads of fun with a financial/HR system that needed to map 12 period fiscal into a 13 period fiscal during transition to a new system.
It makes sense that additions for the default FP variables in BASIC wouldn't have consistent time, considering the 6510 doesn't have hardware FP. (Heck, it doesn't even have integer multiply and divide!) Maybe it would be better to use integer variables, which have names suffixed with "%", if maybe still not perfect (because they're 16-bit on an 8-bit machine, possibly introducing a few cycles difference whenever there's a carry). Just be aware that they're limited to a 16-bit signed range. ...Then again, I would expect the integer math to be much faster and thus require counting to higher numbers for the same delays, so maybe it's not too scalable.
If you start your for loop with a large value like: x=133000:y=x+v*t:for i=x to y:next i then you will remain linear for a large range of seconds. for using the system clock, to make up for the rollback to 0, you can use a pair of loops. You etst if it will overflow, if not you goto the second loop. if it will you start with a loop that waits for the overflow.
What does WAIT X,0 do? And would this work to wait the specified number of jiffies? TI$+"000000" WT=90 WAIT 162,WT AND 128 WAIT 162,WT AND 64 WAIT 162,WT AND 32 WAIT 162,WT AND 16 WAIT 162,WT AND 8 WAIT 162,WT AND 4 WAIT 162,WT AND 2 WAIT 162,WT AND 1
No, this won't work. The first WAIT waits for ever, because the bit with value 128 is not set in WT, the AND gives zero, a "WAIT location,0" never matches because (PEEK(location) AND 0) != 0 won't succeed. This fix this, you must wait for an unset bit, which could be achieved with WAIT 162, 128, NOT(WT) AND 128 WAIT 162, 64, NOT(WT) AND 64 WAIT 162, 32, NOT(WT) AND 32 WAIT 162, 16, NOT(WT) AND 16 WAIT 162, 8, NOT(WT) AND 8 WAIT 162, 4, NOT(WT) AND 4 WAIT 162, 2, NOT(WT) AND 2 WAIT 162, 1, NOT(WT) AND 1 The 3rd parameter of WAIT is the flip mask (xor operation on the location content before and-ing it), which may invert a bit before the "and" mask is applied. Without the flip mask, is is assumed to be zero, which let the value untouched. Just in case we have an unset bit, NOT turns all unset bits into 1, the AND focuses on the bit needed, which finally acts as flip mask. ;)
Just want add another possible wrench to this. I learned of this from a c64 repair vid on YT, where a diags cart tested many things good, but c64 would not run. Someone commented to look at the system clock in the bottom corner on diags, it was frozen. When they seen the part of the schematic, was a bad cap I believe, it uses the 60hz (US)from the input power to make the time clock run on the c64. (That was not expected). So in essence, in UK, would be 50 hz, which would be different numbers. Might be able to see that In emulation. Not sure though.
That only affects the Time Of Day (TOD) clock which (somewhat strangely) both TI and TI$ don't use at all. So PAL/NTSC has no effect on either the TI or WAIT methods. PAL/NTSC does have an impact on the FOR/NEXT method, but not due to the TOD, but rather just the differences in clock speeds and different number of cycles eaten by VIC-II DMA on NTSC and PAL models.
I"m curious if moving joystick or touching keyboard throws off the loop, as the system would get interrupted? incoming modem data may also do that too (I used to write C64 BBS systems).
maybe the problem is that your exponential formula takes more time to compute and the TI variable reflects that, which is an error introduced by the formula itself; also a more simple appproach without all these drifts is: 0 S = 1 10 TI$="000000" 20 IF TI>60*S THEN ? TI : END 30 GOTO 20 S being the time to wait in seconds; you will err on the plus size but hopefully on the 1/100th of a second, consistently above the target wait
20 T=TI+61: IF T>5183999 THEN IF T>TI THEN T=T-5184000 (I did the above line just before you pointed out the overflow issue. Then I updated it. Will the double IF work like that in C64 basic?) TI will automatically wrap so why reset T? Just remove the bad value. It would actually be better to loop it and keep removing 5184000 just in case you have a really bad value but I forget if C64 supports modular arithmetic. If you just need semi bullet proof code then my example should be fine. Also there is a delay in interpreters. You should be joining 10,20 and 30 together. putting the TI="00000000" on the same line as your for loop and then printing it after next will shorten the interpreter delay quite a bit and get a more accurate value. But then you get the argument about the added delay from the C64 basic interpreter anyway? in that case put the TI= on the same line as the for and print the result on the next line. I'm not a C64 basic guy. I was coding a little on APPLE ][s but similar issue are on both.
I was going to ask the same question as your patron and was ready to post it right before you got to that future-future update. I'm surprised it's so nonlinear. I wonder if the time factor would be more predictable if you used integer variables instead of floatingpoint (even though they're slower overall per that other video you did a while ago).
FOR-NEXT does not allow integer variables! Beside of that ever operation is done in floating point, even integer variables are converted to FP and back to store it. ;)
Could you take a number (a) being your time to wait convert it to binary . Now taking that result and set as either 1 or 0 ( b c d e f g h i) as you did set up the truth table 128 ' 64 ' 32 '16 ' 8 ' 4 ' 2 '1 .. then run a if statement to test the logic of b thru I. Running a wait 162 , 128 thru to 1 running the wait if true and not running if false ??? Possible?
19:30 I believe the easiest solution there would be to reset 'ti' if the overflow limit is reached, then repeat the 't' variable assignment. For example, changing line 20 to read: 20 t=ti+61: if t > 5183999 then ti$ = "000000": goto 20 Technically greater accuracy could still be achieved, so I don't know if I'd call it a perfect solution if someone is planning to use their original C64 hardware for 24/7 monitoring of a nuclear reactor 😄. But otherwise, it should be good enough for most practical purposes.
Adjusting the ti counter in the case of an overflow is an interesting direction to take, but in the example you have there when line 20 happens with ti in the last second of 48 hours, the line sticks with ti getting set to 0 on every loop and t just keeps getting 60 jiffies added until some sort of math overflow or exception occurs. But the basic idea seems worth exploring.
@@adymode I believe you misread or misunderstood something. The 'if' conditional does not execute any part of the statement after the 'if' unless the condition is true, which in this case, is only true when the value of 't' is greater than 5183999. If the 'if' conditional is false, that also prevents the final goto statement at the end from being reached, as well. So if the value of 't' is over the limit, the goto will only rerun the statement again one time, to recalculate the value of 't' again. After which, the value of 't' will be set to 0 + 61, which will cause the 'if' statement to be false, and the rest of the statement will not be run again. Execution should continue on normally, to the next line. I'm not beyond mistakes, but I always test my code before I post it, usually even for simple code, just to be sure. But you are welcome to test the code with the rest of his original program. Everything else is the same as the original program, except for line 20, which I posted. If you still think I have an error here, then I'd like to hear where and why you believe it's an error.
The CIA timers, simple. They trigger an interrupt so it's a background activity. The whole idea of doing an exact wait in BASIC is mission impossible from the start😉
I think using a DEF FN is a good solution for pausing for a specific time. 10 DEF FN D(X)=X*920 100 PRINT TI/60 110 FOR I = 0 TO FN D(1) 120 NEXT 130 PRINT TI/60 Seems to work, just pass the time in seconds in the FN T(). I haven't tested this exhaustively, it's like 2AM ....
I also observed a non-linear behavior when the delay is greater than 5 seconds. Next I tried a lookup table but that only works with whole numbers. 10 dim d%(10):gosub 1000 20 printchr$(147) 100 for l =1 to 9 110 a=ti/60 120 for i = 0 to d%(l):next 130 print l;d%(l);ti/60-a 140 next 998 list 999 stop 1000 d%(1)=960:d%(2)=1920:d%(3)=2880 1010 d%(4)=3840:d%(5)=4800:d%(6)=5600 1020 d%(7)=6500:d%(8)=7400:d%(9)=8300 1900 return And these tweaked numbers give interesting results 1 960 1 2 1920 1.98333359 3 2880 3 4 3840 4.03333283 5 4800 5.08333302 6 5600 6 7 6500 7.01666642 8 7400 8.03333283 9 8300 9
I couldn't let it go. I changed the time calculation's place since calculating it after the print is wrong. 1 980 1 2 1970 2 3 2930 3.0166626 4 3850 4 5 4780 5 6 5670 6 7 6570 7 8 7470 8 9 8370 8.96667481 10 dim d%(10):gosub 1000 20 printchr$(147) 100 for l =1 to 9 110 :a=ti/60 120 :for i = 0 to d%(l):next:b=ti/60 130 ::print l;d%(l);b-a 140 next 998 list 999 stop 1000 d%(1)=980::d%(2)=1970:d%(3)=2930 1010 d%(4)=3850:d%(5)=4780:d%(6)=5670 1020 d%(7)=6570:d%(8)=7470:d%(9)=8370 1900 return This seems to work nicely. I guess.
I love the 24 hour endless loop. But realistically .. the C64 would never hold out 24 hours of continous runtime. Either the Power Supply would burn through or the internal chips would burn up because of heat. Most likely it will simply crash :-)
Why would you assume that? It's not like some Windows machine running hundreds of weird processes, some of which may decide to reboot your machine at any time. The amount of power, and thus heat, doesn't change much over time. They've been used for industrial control applications.
Is there a reason for you not using spaces? I know of various BASIC dialects that had more free RAM if not using spaces and others that do not allowed missing space from syntax (FOR I=1TO10 is different than FOR I=1 TO 10). On machines with low RAM (4K) I saw that some BASIC dialects allowed omission of spaces.
Commodore BASIC doesn't require spaces and the manuals actually encouraged programs to be "crunched" as they called it as each space takes an extra byte to store, and a little bit more CPU time to parse. I do try to use spaces more in my videos for better readability, but don't always remember. I think I used spaces in most examples in this video?
Hello, I have written a basic extension that from 49152 is in memory. After loading the programm, however, the pointer on basic end is behind $ C000, so the basic code is not running. Is there a trick? 10 Load "Basic extension", 8.1 20 My Basic Program Code .... 30 .... 40 .... 50 SYS49152, Bla Bla Bla and so forth... I basically only want one basic code that brings a machine program to his right address and then continues to normal in the basic code. I hope you can understand what I mean Thank you Frank from Germany
Everything OK. I found it. Here my result: 10 if peek(49157)=32thengoto30 20 load"window",8,1 30 poke55,0:poke56,160 40 rem******************************* 50 dim a$(40) 60 forx=0to39:readb$:a$(x)=b$:next . . . .
Hi, Robin! I wonder why you have shown cbm calculators but not cbm's first computer the KIM-1? And how it works and it's relation to early PETs (Basic etc.), and do show some programming on the C16/Plus4, C64 is yesterday News in the retroworld, time to move on 😜
Unrelated to this video but a general Commodore question. In CBM Basic, you know those special formatting characters that appear between quotes (ie the Reverse heart for CLR HOME, reverse S for HOME, and other reverse symbols for the different colours, etc). One thing that has puzzled me for decades about my old VIC-20 I had as a kid, between open quotes, the "Run Stop" key shows a reverse C. I've never been able to figure out what that does though (if anything)? I figure if anybody is going to know, it's going to be you. Any insight?
I believe it has to do with the STOP key's relation to the cancel/break functions of other computers. In operating systems like Unix, CP/M, and MS-DOS you hit Control-C to cancel or stop a command, so it makes sense that STOP on the Commodore machines prints a "C" symbol. It doesn't do anything, as far as I know, if you print it to the Commodore's screen, but if you're connected to another machine by serial / modem link, then sending that character may very well cause the receiving computer to respond.
@@the_socially_distant Thanks. I've done some research for an episode just about RUN/STOP so that's when I looked into that particular aspect of it. Hopefully I'll get around to making the video sometime soonish!
Hi, sorry to say that there's no huge significance to the games I put in videos. If there's an empty area on the table in the camera's view, then usually I'll pick a game from my collection and put it in that spot. I looked into it a bit and it seems I've "featured" a game in this way about 90 times out of the ~130 episodes I've made. I usually try to choose a different game each time but sometimes I repeat by accident or on purpose. I think I originally drew a bit of inspiration from Strong Bad's emails, as there was usually a different disk visible each cartoon in his floppy disk holder. Maybe what you're wondering is how I choose the particular game from my collection? Usually I know the subject of the video so I search my shelves looking for something that has some kind of collection. In this case the episode idea was about WAITing, so a Pitstop seemed appropriate.
I have very limited experience with the c64 but couldn't you just setup an interrupt to vector to some routine every second? It is still basic because you would be doing it with peeks and pokes
I was able to get a perfect time delay by simply modifying the string on line 20 below... _One Second Delay:_ 10 ti$="000000" 20 ifti$"000001"then20 30 printti/60 _Five Second Delay:_ 10 ti$="000000" 20 ifti$"000005"then20 30 printti/60 _Ten Second Delay:_ 10 ti$="000000" 20 ifti$"000010"then20 30 printti/60 And so on...
20:48 You could add "15 IF TI>5183939 THEN 15". I know you're shooting for as close to a 1 second delay as possible, and having no delay isn't a valid solution. This way you'll get a delay, but it could be up to 2 seconds, which may be a better solution.
3:40 Why exponential? It is probably meant polynomial, isn't it? As far as I understand maths, X^2 is polynimial and 2^X is exponential. But I have to admit, I'm not a native apeaker, so maybe in English this term might be used for both...
Interesting! I used the word exponential just because I was using exponents rather than linear multiplication but it seems polynomial is the correct word. I googled around and maybe the word "sublinear" is also appropriate?
Yes, non-linear might be also a reasonable term. Sublinear sounds like "constant" to me. If polynomial increase is regarded as bad, an exponential increase is really really bad. A differnce which is significant in comparing P, NP and NP-complete problems the holy grail of problems in the field of informatics. ;)
Could be done in two ways. Init the TOD and poll the value (could be done in a consistent way because of register latching) or with the TOD alarm. The time range would be limited to 1/10 to 1 day, the granularity is 1/10 sec. Only the initialization of the TOD depends on the current frequency (50 or 60 Hz), which could be derived from KERNAL settings.
@@slucas601 I'm using the TOD for time measurements of routines which disables the interrupts ... Here a version to just elapse time, e.g. 10.5 seconds (the end value of FOR T= is the waiting time given in BCD notation (1000 means 00:10:00,0 in HH:MM:SS,T format). For comparison the TI time value is also printed. The greater the delay time the less the overhead and difference between them. This is the PAL version, the NTSC version has to use GOSUB8 in line 10. 6 dimt,d,x:deffnt(x)=int(x/16)*10+(xand15):d=56576:goto10 7 t=fnt(peek(d+11))*1e4+fnt(peek(d+10))*100+fnt(peek(d+9))+fnt(peek(d+8))/10:return 8 poked+14,127andpeek(d+14):poked+15,127andpeek(d+15):poked+11,0:poked+10,0:poked +9,0:poked+8,0:return 9 poked+14,128orpeek(d+14):poked+15,127andpeek(d+15):poked+11,0:poked+10,0:poked +9,0:poked+8,0:return 10 gosub9:t0=ti:printchr$(147) 11 fort=0to10.5step0:gosub7:printchr$(19)t;chr$(157)" ":next 12 print "time elapsed:"t,(ti-t0)/60 Maybe you have to split the line into separate ones because they are to long, or use the abbreviated commands, like 7t=fnt(pE(d+11))*1e4+fnt(pE(d+10))*100+fnt(pE(d+9))+fnt(pE(d+8))/10:reT 8pOd+14,127aNpE(d+14):pOd+15,127aNpE(d+15):pOd+11,0:pOd+10,0:pOd+9,0:pOd+8,0:reT 9pOd+14,128orpE(d+14):pOd+15,127aNpE(d+15):pOd+11,0:pOd+10,0:pOd+9,0:pOd+8,0:reT
@@jjeeeekk Nice work! It's slightly less hideous than I imagined, but still pretty bad. Excellent!! :) Since you put both the PAL and NTSC init in there anyway, I just modified line 10 to automatically call the correct one, assuming the NTSC/PAL flag is set correctly. And yeah, lines 8 and 9 needed every last character squeezed out of them; they're both 80 characters! Nice job. 6dimt,d,x:deffnt(x)=int(x/16)*10+(xand15):d=56576:goto10 7t=fnt(pE(d+11))*1e4+fnt(pE(d+10))*100+fnt(pE(d+9))+fnt(pE(d+8))/10:reT 8pOd+14,127aNpE(d+14):pOd+15,127aNpE(d+15):pOd+11,0:pOd+10,0:pOd+9,0:pOd+8,0:reT 9pOd+14,128orpE(d+14):pOd+15,127aNpE(d+15):pOd+11,0:pOd+10,0:pOd+9,0:pOd+8,0:reT 10on(peek(678)+1)gosub8,9:t0=ti:printchr$(147) 11fort=0to10.5step0:gosub7:printchr$(19)t;chr$(157)" ":next 12print "time elapsed:"t,(ti-t0)/60
On the Commodore 64, jiffies are 1/60th of a second whether PAL or NTSC. They're driven by a timer on the CIA, completely unrelated to the video refresh. You can verify this with a short program such as: 10 ti$="000000" 20 ?ti$;ti:goto20 You'll notice that the jiffy count (TI) on the right side will exceed 50 (it'll probably get up to 57, 58, or 59) while TI$ is still showing 000000. TI$ does not return 000001 (one second) until TI reaches 60 on both NTSC and PAL.
@@8_Bit Hi Robin, interesting! I haven't checked it myself but at least the C64 Wiki is wrong then. It clearly states "In countries using the NTSC standard this is 59.94 Hz (16.8 ms) while in countries using the PAL standard this is 50.0 Hz (20.0 ms)." Thanks for the explanation!
@@branchonequal Aha thanks, I hadn't noticed that entry on C64 Wiki; maybe that's the main source of this misinformation. I see that none of the sources on that page back up the C64-specific assertions made. But there's a big contradiction and proof of 60-jiffies-per-second even on that page. It correctly states that "After the clock is incremented, the number $4F1A01 (or 5184001 jiffies, which is 24 hours + 1 jiffy) is therefore subtracted from the 24-bit count." If 5184001 is 24 hours + 1 jiffy, then we can easily determine that there are 60 jiffies per second, and not 50. 24 hours * 60 minutes * 60 seconds * 50 jiffies + 1 would only be 4320001. 24 * 60 * 60 * 60 jiffies + 1 equals the stated 5184001.
Goodness gracious. So many responses here, and nobody mentions the modulus operators, which is how you deal with overflowing integers. I'm sorry to say, but just about everybody here would not get past a basic programming interview at a company.
It's a little inconvenient to get the time limit value into the proper format for the comparison. You could write it as T=60:T$=STR$(T):T$=RIGHT$("00000"+RIGHT$(T$,LEN(T$)-1),6)
In the last example which seems the optimal method, seems we should be able to fix throwing time off, by just storing TI before start and setting TI$ to correct value after the wait.
"It's terrible, but it works"
This is my career in programming in one sentence.
hehe, I've got a linearly increasing set of POKEs that go up and down from a middle number, something that should be easy to translate over to FOR:NEXT loops but it's working perfectly now, and I really don't want to mess that up. 🙂
Thanks Robin! This was exactly what I was hoping for. Really appreciate the extra time and effort you put in to answering the question as completely as you could!
Hi Robin! Thanks for the video! Many years ago, when I was in school, I made a countdown timer on my TI calculator - to count down the time until our class was over.. It was based on loops, and showed minutes and seconds. It was very imprecise in the beginning, but I kept improving it - when I was bored in class. It got kind of accurate up to one hour!
For the for next method: If the transition to the next power of 2 number is a problem, why not use a range of numbers already in a big power of 2 that won‘t need to transition again for the duration of the loop? Like starting at 1025 instead of 0.
That might do it!
Ooooh, thank you so much for going back and looking at my question!
Thank you for pointing that problem out! That's a problem that can occur in many situations even in modern development so it's great to raise awareness of it.
These times are quite a bit different on a PAL machine. Through trial and error I was able to get them pretty close on a PAL machine.
Also, just a clarification at 6:46: The Ultimate64 can switch anywhere between 1 and 48Mhz (also between PAL and NTSC on the fly), but the Ultimate 1541 (now called the Ultimate II+) does not have a CPU accelerator in it.
21:31 The Toolkit: BASIC book for the win on understanding that. Might be my new favorite set of books.
I was kind of wondering why he never mentioned that the different clock speeds between PAL and NTSC machines would likely cause an issue with the simple loop timer.
Testing with Vice (C64SC), the loop counts I found were 625 for PAL and 780 for NTSC. I also stashed TI into a variable to eliminate PRINT messing with the count, it doesn't seem to impact the jiffie count though.
Does the "IF TI
Was it only the FOR/NEXT loops that were substantially different on PAL? I would think the TI polling and WAIT methods would be the same, at least within a jiffy. But yes, I should have mentioned PAL. No matter how thorough I think I'm being, and how deep I go down the rabbit hole, there's always more :)
@@8_Bit Again testing with Vice "C64SC" both polling and WAIT works as expected you just have to adjust for 50 jiffies in a second. I think WAIT method is the better choice when you need that level of accuracy, which on the C64 is exceedingly rare.
So they're like Gremlins, don't run the loop after midnight.
0:03 I'd like to know how to achieve a sleep(-1).
6:41 You'll be happy to hear that the SuperCPU I recycled several years ago wasn't destroyed but was kept by someone and recently sold to a professor at the University of Windsor.
6:50 The C-128 has a method of boosting the CPU speed also…
7:19 Did they called that the "Double Future" in "Bill & Ted's Excellent Adventure"?
10:37 So it is the extra bit-shifting to align the mantissas for adding that's causing the difference. Your count-limit formula will need a base-2 logarithm in it!
10:56 When X is at 15, it doesn't need to align the values as far to add the 1, but then the result is 16, which will need to be normalized by shifting the mantissa and incrementing the exponent of the result.
11:38 The 32-bit CPUs had a barrel shifter, which can shift any number of bits in linear time. Of course, they also had direct hardware floating-point support.
16:58 Is "5184000" a KERNAL bug? I don't think it should ever show that. It'd be like a clock going from 23:59:59 to 24:00:00 a second later and then going to 0:00:00 the second after that, giving the day an extra second.
20:57 Just because it CAN be done, doesn't mean it SHOULD be done!
28:50 It's just the same bits shifted left one position.
31:12 reminds me of the DankPods credits: ua-cam.com/video/uuS_ZioCfDo/v-deo.html .
That's great your old SuperCPU still exists! It seems like there might be an interesting story behind how you came to know this?
That's true that 5184000 seems like it shouldn't exist. Oh, I just did some quick experiments, and if you repeatedly print TI$ quickly when it's about to wrap, then if you're lucky you'll see time "240000" printed once between many "235959" and "000000"! Certainly seems like a bug, but probably only adds 1/60th of a second to the day, instead of a full second.
Seems I have to speed up the credits a LOT more to match DankPods. I usually have some reason behind how I customize the credits each episode. This time I (re)discovered a stash of my music that I wanted to use, and when I found this song we deliberately recorded to be only 30 seconds, I thought it would be an amusing contrast in a video all about WAITing to do anything but with the credits.
@@8_Bit: I simply had put my name on my old Commodore equipment and the buyer either was familiar with my name or looked it up, perhaps to see if the items were stolen.
This is yet another great video! Thank you! Also the segment that touches on floating point number(s) normalization is fantastic.
A user on one of my BBSes said I needed to come check out your channel. Thank you for your contributions to the C= community! :D
5 second delay: 10 S=TI:EN=S+60*5:OF=0 20 IF TI
The TI check wraparound is a modulo arithmetic problem. Finding the target threshold is easy; t=TI+d : if t>max t=t-max. The problem is there's time before the wraparound happens, so TI>t already. So you'd wait for TIt. It's a bunch of logic. Furthermore, it doesn't solve the issue of when t is close to max, and we may have no jiffies when TI>t. Checking for TI=t, as you might do in hardware, wouldn't do if basic might be too slow to read TI at the right jiffy (though maybe it is fast enough if you're not printing).
The more generic form is comparing (TI-oldTI)%max to delta. Unfortunately there's no modulo operator in the C64 basic, afaik. So you'd calculate dt=(TI-oldTI), then fix it up by adding max if dt
My attempt at this is:
1 X=120 (Whatever the delay is in Jiffies)
10 TI$="235959" (Set Jiffy Count for testing)
20 PRINT TI
30 T=TI+X
40 IF T
Nice, this is similar to my attempt. I would just simply this formula for clarity: T=X-(5184000-TI)-5 to T=TI+X-(5184000+5) . I guess those 5 jiffies are to compensate for TI changing between the execution of first T calculation and re-calculation. Im not sure if just T=T-5184000 would suffice there.
Instead of TI 0 THEN 30
Simple and easy - and precise:
10 INPUT W
20 TI$="000000"
30 IF TI/60
It was really cool to see the walkthrough of each of these approaches. I immediately thought of the polling TI method, but seeing that bug was interesting. Great video.
10 T=TI+59
20 IF T>=5184001 THEN T=T-5184001
30 IF TT THEN 30
40 IF TI
Regarding the midnight wrap-around, IBM BASIC had a similar problem when using the TIMER function for delays, and I had an old BASIC reference book that suggested a delay routine that could properly handle the wrapping problem. It looked roughly like this:
(TIMER returns current system time in seconds as a float, mod 86400)
seconds = (number of seconds to wait)
t = TIMER
if 86400 - t < seconds + .15 then t = t - 86400: while timer > 0: wend
while TIMER < t + seconds: wend
It seems like this could be adapted to fit the C64 case. What it seems to do (if I'm interpreting the code correctly) is determine first whether the system timer would overflow during the delay. If not, it delays in pretty much the same way as given in the video. If it will (presumably the .15 in the expression "86400 - t < seconds + .15" is just a safety net to compensate for BASIC execution overhead), then it appears to loop until the system timer has overflowed first and adjusts the value of t so that when it goes to the second while loop (the usual delay loop) it ends up waiting the correct amount of time that remains for the delay *after* the overflow (that is, the total time to wait minus the amount of time it took until the timer overflowed).
my solution to wait using the TI variable and handling the wrap around:
5 TI$ = "235959"
10 PRINT TI
15 S = TI : REM STORE START TIME IN S
20 T = S + 60 : REM STOR END TIME IN T
25 IF T >= 5184000 THEN 50 : REM OVERFL
30 IF TI < T THEN 30
35 GOTO 100
50 T = T - 5184000 : REM WRAP AROUND T
60 IF TI >= S OR TI < T THEN 60
100 PRINT TI
Would a nested loop with outer loop being the number of required seconds and inner being the one second delay. It wouldn't be doing math with big numbers, it might keep it more stable? Thanks for the video Robin, the "directors cut" is making me think more!
Hm... Yeah - esp. when adding a ~calibration loop when compensating for ~processing speed across different machines?
Great vid!! I never used the Wait command back in the '80s as a kid programming on my uncle's c64 because I didn't understand what the operands even were. Logically, you'd think it'd be one operand with the delay in milliseconds or jiffies etc. But no! Now thanks to this vid, I understand it basically watches an address for a bit.
Ok, how about this: let's suppose you want to make something happen at regular (ish) intervals, but want other code to execute in the meantime? I suggest something like:
50 D=128:D1=5: D=int(D/4): Rem delay in Jiffies, seconds (ish)
100 T=Peek(162)+D:T1=Peek(161)+D1
... other code...
500 TF = Peek(161): If TF>T1 Then Gosub RegularRoutine
510 if TF = T1 And Peek(162) >T then Gosub RegulaRoutine
520 Goto 500
Perhaps the delay in address 162 is too granular relative to the expected time to complete the ...other code.... bit so that part can be omitted simplifying the setup and checking.
As long as there aren't any long loops (significantly longer than the delay vaules) this works kind of like an interrupt routine. In fact, instead of once per loop, there could be multiple checks occuring throughout the ...other code... section if necessary.
Might be an interesting topic: how to simulate interrupts in basic?
Thanks for the amazing content as always.
I love these videos. A lot of trial and error and multiple solutions to the same problem. This is how I remember getting started with programming. Just "messing" around like this. Great way of learning I think!
Noticing that the TI max value went to 5184000 not 5183999, I tried printed TI$ while wrapping midnight repeatedly and you actually do get "240000" briefly! :-)
I seem to recall that they made this simpler in BASIC 7.0 for the 128, but I'd have to dig out my old books.
Yep, it's simply SLEEP . Annoyingly though it only sleeps for integer seconds (3 is the same as 3.5 for SLEEP)... :C
Damn ... as soon as i saw the 4 bit pattern it hits me like the guy on twitter ... now i'm happy that after 34 years i'm still into this crap 😀
Did something similar, just used string to get the time, and then looped till seconds time rolled over to 0, then waited in another string till seconds time rolled over to 2, before exiting. Got me a good 1 minute count routine, which I was using a lot, calling it for delays.
Then did some IO, and counted minutes again. Was using this to run some automation to duplicate tapes, outer loop ran the number of total machine cycles, then the next loop ran the machine cycle, for a complete run of the machine, which was variable set at start. Then at the end it stopped, rewound the tape to the beginning ( fixed time, not worth doing anything more complex), and ejected, which also loaded the new tape. Then emulated pressing record and play, and waited for the cycle time again.
An update for the wait loop, as I had a lot of time, was to use it to clear the screen every 30 seconds (look in string for seconds equal to 30) and randomly choose a screen location, and print the assorted timers in the system. Glitchy at 30 seconds, so that got a short roughly 1 second for next loop to not spam the screen, and again at the 0 second mark, implementing a simple screen saver for the well used mono HGC monitor I was using for output.
Ran for a good number of years with no issue, replacing an original controller that was unreliable and temperamental on it's good days, and downright ornery when it was not. I got tired of fixing this thing, as timings were all set using really long RC delays, using 555 timers and 10000uf capacitors, which was not at all repeatable. Instead looked at the pile of old PC's, and chose a nice reasonably new 386SX16 motherboard that I had 2 spares of, put in the requisite video card, keyboard, 3.5in diskette drive, and MSDOS 6.22. Used a parallel port card as the IO, writing direct to the port address to use the bits to emulate the keys on the wired Matsushita remote control for all functions. Controllers in the case, and powered off the 12V rail, as they had on board regulators, and a bit of software, and the original controller was dismantled.
Still got the computer, but no monitor for it, though I still have a keyboard that will work, and it likely will still boot and run.
10 TI$="000000"
20 FOR I=0 TO 50:WAIT 53265,128:NEXT
30 PRINT TI/60
Here I'm counting the frames processed in one second. Alter the value in the FOR statement 50 for PAL, 60 for NSTC. 🙂
5 TI$="235959"
10 PRINT TI
20 Q=TI:T=TI+61
25 IF V=0 THEN IF TI
Ingenious mechanism. Only drawback is the overall loop would cycle less frequently than the simple 'IF TI
@@adymode Thanks. It could be optimized a bit by only adding the day carry when needed. I haven't tested it yet, but I assume an extra test and jump might be quicker than the floating point addition. You can do quite a bit of optimizing when you know the ins and outs of C64 BASIC, but optimized code usually isn't very pretty.
Hugo's hint was amazingly clever
Dave from Dave's Garage had a really great job explaining floating point math. It's the first time I ever heard the word "mantissa." I don't know how I went all this time without knowing that word. Anyway, the funny thing was that the video wasn't even about floating point math. He just covered it to explain something else. I believe it's in the second part of his two parter about "Quake III's Secret Algorithm."
I don't know C64 Basic, but in QBasic I always used TIMER, which is just like the TI$, except TIMER counts in milliseconds. Just reference TIMER for your wait loops and you can time things down to almost the millisecond. QBasic code is never quite fast enough to catch the exact millisecond, but you can get within 3 or 4 on a typical 486 of the day.
New lines take longer to process than semicolons, so to get a more accurate reading you should use
10 TI$="000000":FOR I=1 TO 1000:NEXT:?TI
This will show the loop to take 63 jiffies instead of 64 when using seperate lines.
Lol, PitStopII - I loved it... hours spent with a friend on a split screen, racing... :D
Kinda surprised one of the TOD clocks wasn't used. That's how I handle timeouts in ML. It also has the advantage of not being disrupted by I/O.
Seems like it'd be pretty ugly to use the TOD from BASIC, but I've never tried. The numbers are in BCD, aren't they? Polling them would be pretty bad, and since seconds are separate from the 1/10th of a second, we'd have to read two registers to do accurate timing. Seems like an interesting solution for ML and GEOS programming, but it really doesn't seem appropriate to me for BASIC unless I'm missing something.
@@8_Bit I posted an example in @Shawn Lucas comment "How about reading the TOD registers on the 6526 IC?"
This variant is very useful to benchmark routines which inherently run with disabled interrupts ...
@@8_Bit It's easy enough to set all bytes to zero and use it for elapsed time.
I remember using FOR NEXT exclusively for delays, then my dad blowing my mind by showing that stuff could go in-between the FOR and NEXT...
Even more fun, you can modify the value of the index in the loop to cause it to exit or never exit, like a hacked do... while kinda thing.
There's probably no way to fully fix your wrap-around problem (you're always going to have a single shortened delay time just before midnight), but you can easily avoid the infinite loop by re-arranging your test. This is how I usually do this sort of thing:
20 T=TI
30 IF ABS(TI-T) < 60 then 30
When TI wraps to 0, ABS(TI-T) will become a large positive number, and the loop will terminate.
Those slight divide errors are known as floating point drift and are endemic on machines lacking math coprocessors. This is why computer makers later added math chips.
Without changing the clock:
10 T=(PEEK(162)+60) and 255
20 IF PEEK(162)T THEN 20
(although if there are TSRs running in the background this _could_ fail - but testing under VICE it works reliably in normal circumstances with CPU running anywhere down to 10% of normal).
Although:
clc
lda 162
adc #60
cmp 162
bne 252
rts
in hex: 18 a5 a2 69 3c c5 a2 d0 fc 60
in decimal: 24 165 162 105 60 197 162 208 252 96
compiles down to 10 bytes (arguably you could omit the 'clc' without much error, perhaps it would pause for an extra 1/60 of a second). The code is relocatable, so poke it somewhere available (49152 to 49161) and sys it (sys 49152) when you need a (pretty darn accurate) one second delay.
Thanks for this. Explains a lot about something that "seems simple," but isn't.
I think he was trying to get a more definitive method of specifying a certain time, rather than a loop that could have different time lengths depending on CPU load, etc.
But I agree, for general purposes the FOR:NEXT loops are definitely the easier method. @@Joel-qz6sd
And finally I understand the WAIT command.
The instruction manual I got with my C64 did an absolutely terrible job of explaining this. Commodore did a terrible disservice to their machine by not shipping it with the far more comprehensive user's guide; it might have kept more of its younger users into programming and away from games (that they didn't write themselves).
Super helpful for what I am about to do. Thanks! Great shows!
Busy wait FOR-loops are evil. It's not just about them running faster on faster CPUs, but in compiled code as well. In fact, my BASIC cross compiler detects that these are empty loops and replaces them by setting the variable's value directly to the final value by default...and there goes your wait...:-)
Nice video! Thank you.
Essentially similar to the final "Another WAIT approach", if you set TI$="000047", then run WAIT 162,64, it should work for one second because it also sets the timer's low byte to 4. POKEing 162 is probably more elegant!
All of these future Robins could make a whole cinematic universe, good video tho!
@11:50 that should be interesting! In my experience, no two floating point libraries ever work exactly the same. Had loads of fun with a financial/HR system that needed to map 12 period fiscal into a 13 period fiscal during transition to a new system.
Don't blame floating point for not being able to exactly represent irrational numbers. It's not even possible.
That quirky WAIT behavior has bit me once in the past.
It makes sense that additions for the default FP variables in BASIC wouldn't have consistent time, considering the 6510 doesn't have hardware FP. (Heck, it doesn't even have integer multiply and divide!) Maybe it would be better to use integer variables, which have names suffixed with "%", if maybe still not perfect (because they're 16-bit on an 8-bit machine, possibly introducing a few cycles difference whenever there's a carry). Just be aware that they're limited to a 16-bit signed range. ...Then again, I would expect the integer math to be much faster and thus require counting to higher numbers for the same delays, so maybe it's not too scalable.
Here's my solution to the TI Epoch bug
10 TI$="235959"
20 PRINT TI
30 T=TI+61
40 IF TI=0 AND T>5184000 THEN T=T-5184000
50 IF TI
If you start your for loop with a large value like: x=133000:y=x+v*t:for i=x to y:next i then you will remain linear for a large range of seconds. for using the system clock, to make up for the rollback to 0, you can use a pair of loops. You etst if it will overflow, if not you goto the second loop. if it will you start with a loop that waits for the overflow.
What does WAIT X,0 do? And would this work to wait the specified number of jiffies?
TI$+"000000"
WT=90
WAIT 162,WT AND 128
WAIT 162,WT AND 64
WAIT 162,WT AND 32
WAIT 162,WT AND 16
WAIT 162,WT AND 8
WAIT 162,WT AND 4
WAIT 162,WT AND 2
WAIT 162,WT AND 1
No, this won't work. The first WAIT waits for ever, because the bit with value 128 is not set in WT, the AND gives zero, a "WAIT location,0" never matches because (PEEK(location) AND 0) != 0 won't succeed.
This fix this, you must wait for an unset bit, which could be achieved with
WAIT 162, 128, NOT(WT) AND 128
WAIT 162, 64, NOT(WT) AND 64
WAIT 162, 32, NOT(WT) AND 32
WAIT 162, 16, NOT(WT) AND 16
WAIT 162, 8, NOT(WT) AND 8
WAIT 162, 4, NOT(WT) AND 4
WAIT 162, 2, NOT(WT) AND 2
WAIT 162, 1, NOT(WT) AND 1
The 3rd parameter of WAIT is the flip mask (xor operation on the location content before and-ing it), which may invert a bit before the "and" mask is applied. Without the flip mask, is is assumed to be zero, which let the value untouched. Just in case we have an unset bit, NOT turns all unset bits into 1, the AND focuses on the bit needed, which finally acts as flip mask. ;)
Just want add another possible wrench to this. I learned of this from a c64 repair vid on YT, where a diags cart tested many things good, but c64 would not run. Someone commented to look at the system clock in the bottom corner on diags, it was frozen. When they seen the part of the schematic, was a bad cap I believe, it uses the 60hz (US)from the input power to make the time clock run on the c64. (That was not expected). So in essence, in UK, would be 50 hz, which would be different numbers. Might be able to see that In emulation. Not sure though.
That only affects the Time Of Day (TOD) clock which (somewhat strangely) both TI and TI$ don't use at all. So PAL/NTSC has no effect on either the TI or WAIT methods. PAL/NTSC does have an impact on the FOR/NEXT method, but not due to the TOD, but rather just the differences in clock speeds and different number of cycles eaten by VIC-II DMA on NTSC and PAL models.
I"m curious if moving joystick or touching keyboard throws off the loop, as the system would get interrupted? incoming modem data may also do that too (I used to write C64 BBS systems).
I’d just wait ‘X’*60 jiffy’s in an infinite loop, then exit when ‘TI=X*60’ (assuming 1 jiffy=1/60th sec, and ‘X’ is the desired delay in seconds.) 😌
Don't need to be a loop. Just check in one line and go to the same line as long as TI hasn't reached the value
maybe the problem is that your exponential formula takes more time to compute and the TI variable reflects that, which is an error introduced by the formula itself; also a more simple appproach without all these drifts is:
0 S = 1
10 TI$="000000"
20 IF TI>60*S THEN ? TI : END
30 GOTO 20
S being the time to wait in seconds; you will err on the plus size but hopefully on the 1/100th of a second, consistently above the target wait
20 T=TI+61: IF T>5183999 THEN IF T>TI THEN T=T-5184000
(I did the above line just before you pointed out the overflow issue. Then I updated it. Will the double IF work like that in C64 basic?)
TI will automatically wrap so why reset T? Just remove the bad value. It would actually be better to loop it and keep removing 5184000 just in case you have a really bad value but I forget if C64 supports modular arithmetic. If you just need semi bullet proof code then my example should be fine.
Also there is a delay in interpreters. You should be joining 10,20 and 30 together. putting the TI="00000000" on the same line as your for loop and then printing it after next will shorten the interpreter delay quite a bit and get a more accurate value. But then you get the argument about the added delay from the C64 basic interpreter anyway? in that case put the TI= on the same line as the for and print the result on the next line.
I'm not a C64 basic guy. I was coding a little on APPLE ][s but similar issue are on both.
Future Robin really had his work cut out for him on this one.🤣
All this video is missing is a DeLorean :)
Wouldn't POKE162,4:WAIT162,64 give a 1 second delay every time, without the need to reset TI$ or to use multiple WAIT in sequence?
I wish I knew that 40 years ago
I'd love to see if these approaches works the same way on basic 3.5 and basic 7.0, 264 and 128 machines respectively.
I was going to ask the same question as your patron and was ready to post it right before you got to that future-future update. I'm surprised it's so nonlinear. I wonder if the time factor would be more predictable if you used integer variables instead of floatingpoint (even though they're slower overall per that other video you did a while ago).
FOR-NEXT does not allow integer variables! Beside of that ever operation is done in floating point, even integer variables are converted to FP and back to store it. ;)
10 T=3:TI$="000000"
20 IF TI/60
What about using the clock timer in the CIAs?
This video makes me feel like Doc Emmett Brown. ;)
Could you take a number (a) being your time to wait convert it to binary . Now taking that result and set as either 1 or 0 ( b c d e f g h i) as you did set up the truth table 128 ' 64 ' 32 '16 ' 8 ' 4 ' 2 '1 .. then run a if statement to test the logic of b thru I. Running a wait 162 , 128 thru to 1 running the wait if true and not running if false ??? Possible?
19:30 I believe the easiest solution there would be to reset 'ti' if the overflow limit is reached, then repeat the 't' variable assignment. For example, changing line 20 to read:
20 t=ti+61: if t > 5183999 then ti$ = "000000": goto 20
Technically greater accuracy could still be achieved, so I don't know if I'd call it a perfect solution if someone is planning to use their original C64 hardware for 24/7 monitoring of a nuclear reactor 😄. But otherwise, it should be good enough for most practical purposes.
Adjusting the ti counter in the case of an overflow is an interesting direction to take, but in the example you have there when line 20 happens with ti in the last second of 48 hours, the line sticks with ti getting set to 0 on every loop and t just keeps getting 60 jiffies added until some sort of math overflow or exception occurs. But the basic idea seems worth exploring.
@@adymode I believe you misread or misunderstood something. The 'if' conditional does not execute any part of the statement after the 'if' unless the condition is true, which in this case, is only true when the value of 't' is greater than 5183999. If the 'if' conditional is false, that also prevents the final goto statement at the end from being reached, as well. So if the value of 't' is over the limit, the goto will only rerun the statement again one time, to recalculate the value of 't' again. After which, the value of 't' will be set to 0 + 61, which will cause the 'if' statement to be false, and the rest of the statement will not be run again. Execution should continue on normally, to the next line. I'm not beyond mistakes, but I always test my code before I post it, usually even for simple code, just to be sure. But you are welcome to test the code with the rest of his original program. Everything else is the same as the original program, except for line 20, which I posted. If you still think I have an error here, then I'd like to hear where and why you believe it's an error.
@@nunyobiznez875 Oh yes of course ! I see now, thanks for explaining. That is a great tweak.
lol once you explained what wait was doing I'm like.......you could wait for each bit, but that would suck..........and then you actually did it. LMAO
Now a (1s) delay in assembly, that works at C64 (NTSC/PAL), 1MHz and 48MHz speeds.
The CIA timers, simple. They trigger an interrupt so it's a background activity. The whole idea of doing an exact wait in BASIC is mission impossible from the start😉
You could use the TI value (memory locations 160-162) in the same way as with the TI
I think using a DEF FN is a good solution for pausing for a specific time.
10 DEF FN D(X)=X*920
100 PRINT TI/60
110 FOR I = 0 TO FN D(1)
120 NEXT
130 PRINT TI/60
Seems to work, just pass the time in seconds in the FN T().
I haven't tested this exhaustively, it's like 2AM ....
I also observed a non-linear behavior when the delay is greater than 5 seconds.
Next I tried a lookup table but that only works with whole numbers.
10 dim d%(10):gosub 1000
20 printchr$(147)
100 for l =1 to 9
110 a=ti/60
120 for i = 0 to d%(l):next
130 print l;d%(l);ti/60-a
140 next
998 list
999 stop
1000 d%(1)=960:d%(2)=1920:d%(3)=2880
1010 d%(4)=3840:d%(5)=4800:d%(6)=5600
1020 d%(7)=6500:d%(8)=7400:d%(9)=8300
1900 return
And these tweaked numbers give interesting results
1 960 1
2 1920 1.98333359
3 2880 3
4 3840 4.03333283
5 4800 5.08333302
6 5600 6
7 6500 7.01666642
8 7400 8.03333283
9 8300 9
Maybe not non-linear, more like an inflection point at around 5 seconds.
I couldn't let it go. I changed the time calculation's place since calculating it after the print is wrong.
1 980 1
2 1970 2
3 2930 3.0166626
4 3850 4
5 4780 5
6 5670 6
7 6570 7
8 7470 8
9 8370 8.96667481
10 dim d%(10):gosub 1000
20 printchr$(147)
100 for l =1 to 9
110 :a=ti/60
120 :for i = 0 to d%(l):next:b=ti/60
130 ::print l;d%(l);b-a
140 next
998 list
999 stop
1000 d%(1)=980::d%(2)=1970:d%(3)=2930
1010 d%(4)=3850:d%(5)=4780:d%(6)=5670
1020 d%(7)=6570:d%(8)=7470:d%(9)=8370
1900 return
This seems to work nicely. I guess.
Wow, that TI$ was new to me
I love the 24 hour endless loop. But realistically .. the C64 would never hold out 24 hours of continous runtime. Either the Power Supply would burn through or the internal chips would burn up because of heat. Most likely it will simply crash :-)
Why would you assume that? It's not like some Windows machine running hundreds of weird processes, some of which may decide to reboot your machine at any time. The amount of power, and thus heat, doesn't change much over time. They've been used for industrial control applications.
so WAIT x,y is sort of If (PEEK x) = y. IF statements are usually slower than other commands. WAIT is useful maybe for none branching code?
No, its equivalent with 10 IF (PEEK(x) AND Y) = 0 THEN 10
Its a bitwise check of mask Y, not a comparison by value.
Would the WAIT command method give reliable results on a PAL C64 running at .985mhz and with a 50hz refresh rate?
Is there a reason for you not using spaces?
I know of various BASIC dialects that had more free RAM if not using spaces and others that do not allowed missing space from syntax (FOR I=1TO10 is different than FOR I=1 TO 10).
On machines with low RAM (4K) I saw that some BASIC dialects allowed omission of spaces.
Commodore BASIC doesn't require spaces and the manuals actually encouraged programs to be "crunched" as they called it as each space takes an extra byte to store, and a little bit more CPU time to parse. I do try to use spaces more in my videos for better readability, but don't always remember. I think I used spaces in most examples in this video?
Hello, I have written a basic extension that from 49152 is in memory. After loading the programm, however, the pointer on basic end is behind $ C000, so the basic code is not running. Is there a trick?
10 Load "Basic extension", 8.1
20 My Basic Program Code ....
30 ....
40 ....
50 SYS49152, Bla Bla Bla
and so forth...
I basically only want one basic code that brings a machine program to his right address and then continues to normal in the basic code.
I hope you can understand what I mean
Thank you
Frank from Germany
Everything OK. I found it. Here my result:
10 if peek(49157)=32thengoto30
20 load"window",8,1
30 poke55,0:poke56,160
40 rem*******************************
50 dim a$(40)
60 forx=0to39:readb$:a$(x)=b$:next
.
.
.
.
I REALLY LOVE C64 👍🥂🎩
Hi, Robin! I wonder why you have shown cbm calculators but not cbm's first computer the KIM-1? And how it works and it's relation to early PETs (Basic etc.), and do show some programming on the C16/Plus4, C64 is yesterday News in the retroworld, time to move on 😜
If anyone wants to give me a KIM-1 I'll gladly show it! Have you got one for me? :) But I do happen to have a Plus/4 video coming very soon.
they are available as they were from MOS (as a kit computer) for roughly 10$ by obscelence guaranteed it's called KIM-Uno
Unrelated to this video but a general Commodore question. In CBM Basic, you know those special formatting characters that appear between quotes (ie the Reverse heart for CLR HOME, reverse S for HOME, and other reverse symbols for the different colours, etc). One thing that has puzzled me for decades about my old VIC-20 I had as a kid, between open quotes, the "Run Stop" key shows a reverse C. I've never been able to figure out what that does though (if anything)? I figure if anybody is going to know, it's going to be you. Any insight?
I believe it has to do with the STOP key's relation to the cancel/break functions of other computers. In operating systems like Unix, CP/M, and MS-DOS you hit Control-C to cancel or stop a command, so it makes sense that STOP on the Commodore machines prints a "C" symbol. It doesn't do anything, as far as I know, if you print it to the Commodore's screen, but if you're connected to another machine by serial / modem link, then sending that character may very well cause the receiving computer to respond.
@@8_Bit Very interesting! I never thought about that, that seems very logical. I've learned a lot watching your videos!
@@the_socially_distant Thanks. I've done some research for an episode just about RUN/STOP so that's when I looked into that particular aspect of it. Hopefully I'll get around to making the video sometime soonish!
@@8_Bit I will look forward to it!
ultimate 1541 2+ at least does not have a CPU boost function. As far as I know only the U64 has that in the ultimate product line.
@8 Bit Show and Tell, what's the story about the Pitstop II box in the video, I sat through this a second time and still don't get it. Thanks.
Hi, sorry to say that there's no huge significance to the games I put in videos. If there's an empty area on the table in the camera's view, then usually I'll pick a game from my collection and put it in that spot. I looked into it a bit and it seems I've "featured" a game in this way about 90 times out of the ~130 episodes I've made. I usually try to choose a different game each time but sometimes I repeat by accident or on purpose. I think I originally drew a bit of inspiration from Strong Bad's emails, as there was usually a different disk visible each cartoon in his floppy disk holder. Maybe what you're wondering is how I choose the particular game from my collection? Usually I know the subject of the video so I search my shelves looking for something that has some kind of collection. In this case the episode idea was about WAITing, so a Pitstop seemed appropriate.
@@8_Bit WAITing == Pitstop, yes funny
I have very limited experience with the c64 but couldn't you just setup an interrupt to vector to some routine every second? It is still basic because you would be doing it with peeks and pokes
It's possible, but I'm just looking at "pure" BASIC solutions in this video that don't use any machine language.
Future Robin should get his own channel if he's going to interrupt so much.
I was able to get a perfect time delay by simply modifying the string on line 20 below...
_One Second Delay:_
10 ti$="000000"
20 ifti$"000001"then20
30 printti/60
_Five Second Delay:_
10 ti$="000000"
20 ifti$"000005"then20
30 printti/60
_Ten Second Delay:_
10 ti$="000000"
20 ifti$"000010"then20
30 printti/60
And so on...
Any thoughts, Robin?
20:48 You could add "15 IF TI>5183939 THEN 15". I know you're shooting for as close to a 1 second delay as possible, and having no delay isn't a valid solution. This way you'll get a delay, but it could be up to 2 seconds, which may be a better solution.
I use my iPhone timer for this.
👍
Does the program have some load on the time counting?
Aaaand he answered my question at around 15 minutes.
So there is more than one way to "skin a cat". ; )
3:40 Why exponential? It is probably meant polynomial, isn't it? As far as I understand maths, X^2 is polynimial and 2^X is exponential. But I have to admit, I'm not a native apeaker, so maybe in English this term might be used for both...
Interesting! I used the word exponential just because I was using exponents rather than linear multiplication but it seems polynomial is the correct word. I googled around and maybe the word "sublinear" is also appropriate?
Yes, non-linear might be also a reasonable term. Sublinear sounds like "constant" to me. If polynomial increase is regarded as bad, an exponential increase is really really bad. A differnce which is significant in comparing P, NP and NP-complete problems the holy grail of problems in the field of informatics. ;)
How about reading the TOD registers on the 6526 IC?
Could be done in two ways. Init the TOD and poll the value (could be done in a consistent way because of register latching) or with the TOD alarm.
The time range would be limited to 1/10 to 1 day, the granularity is 1/10 sec.
Only the initialization of the TOD depends on the current frequency (50 or 60 Hz), which could be derived from KERNAL settings.
@@jjeeeekk example or tutorial?
@@slucas601 I'm using the TOD for time measurements of routines which disables the interrupts ... Here a version to just elapse time, e.g. 10.5 seconds (the end value of FOR T= is the waiting time given in BCD notation (1000 means 00:10:00,0 in HH:MM:SS,T format). For comparison the TI time value is also printed. The greater the delay time the less the overhead and difference between them. This is the PAL version, the NTSC version has to use GOSUB8 in line 10.
6 dimt,d,x:deffnt(x)=int(x/16)*10+(xand15):d=56576:goto10
7 t=fnt(peek(d+11))*1e4+fnt(peek(d+10))*100+fnt(peek(d+9))+fnt(peek(d+8))/10:return
8 poked+14,127andpeek(d+14):poked+15,127andpeek(d+15):poked+11,0:poked+10,0:poked +9,0:poked+8,0:return
9 poked+14,128orpeek(d+14):poked+15,127andpeek(d+15):poked+11,0:poked+10,0:poked +9,0:poked+8,0:return
10 gosub9:t0=ti:printchr$(147)
11 fort=0to10.5step0:gosub7:printchr$(19)t;chr$(157)" ":next
12 print "time elapsed:"t,(ti-t0)/60
Maybe you have to split the line into separate ones because they are to long, or use the abbreviated commands, like
7t=fnt(pE(d+11))*1e4+fnt(pE(d+10))*100+fnt(pE(d+9))+fnt(pE(d+8))/10:reT
8pOd+14,127aNpE(d+14):pOd+15,127aNpE(d+15):pOd+11,0:pOd+10,0:pOd+9,0:pOd+8,0:reT
9pOd+14,128orpE(d+14):pOd+15,127aNpE(d+15):pOd+11,0:pOd+10,0:pOd+9,0:pOd+8,0:reT
@@jjeeeekk Nice work! It's slightly less hideous than I imagined, but still pretty bad. Excellent!! :)
Since you put both the PAL and NTSC init in there anyway, I just modified line 10 to automatically call the correct one, assuming the NTSC/PAL flag is set correctly. And yeah, lines 8 and 9 needed every last character squeezed out of them; they're both 80 characters! Nice job.
6dimt,d,x:deffnt(x)=int(x/16)*10+(xand15):d=56576:goto10
7t=fnt(pE(d+11))*1e4+fnt(pE(d+10))*100+fnt(pE(d+9))+fnt(pE(d+8))/10:reT
8pOd+14,127aNpE(d+14):pOd+15,127aNpE(d+15):pOd+11,0:pOd+10,0:pOd+9,0:pOd+8,0:reT
9pOd+14,128orpE(d+14):pOd+15,127aNpE(d+15):pOd+11,0:pOd+10,0:pOd+9,0:pOd+8,0:reT
10on(peek(678)+1)gosub8,9:t0=ti:printchr$(147)
11fort=0to10.5step0:gosub7:printchr$(19)t;chr$(157)" ":next
12print "time elapsed:"t,(ti-t0)/60
@@8_Bit Your initialization switch is pretty nice and handy too. :)
With PAL machines, are jiffy's 1/50th of a second?
Yes!
On the Commodore 64, jiffies are 1/60th of a second whether PAL or NTSC. They're driven by a timer on the CIA, completely unrelated to the video refresh. You can verify this with a short program such as:
10 ti$="000000"
20 ?ti$;ti:goto20
You'll notice that the jiffy count (TI) on the right side will exceed 50 (it'll probably get up to 57, 58, or 59) while TI$ is still showing 000000. TI$ does not return 000001 (one second) until TI reaches 60 on both NTSC and PAL.
@@branchonequal No, as Robin already explained it.
@@8_Bit Hi Robin, interesting! I haven't checked it myself but at least the C64 Wiki is wrong then. It clearly states
"In countries using the NTSC standard this is 59.94 Hz (16.8 ms) while in countries using the PAL standard this is 50.0 Hz (20.0 ms)."
Thanks for the explanation!
@@branchonequal Aha thanks, I hadn't noticed that entry on C64 Wiki; maybe that's the main source of this misinformation. I see that none of the sources on that page back up the C64-specific assertions made. But there's a big contradiction and proof of 60-jiffies-per-second even on that page. It correctly states that "After the clock is incremented, the number $4F1A01 (or 5184001 jiffies, which is 24 hours + 1 jiffy) is therefore subtracted from the 24-bit count." If 5184001 is 24 hours + 1 jiffy, then we can easily determine that there are 60 jiffies per second, and not 50. 24 hours * 60 minutes * 60 seconds * 50 jiffies + 1 would only be 4320001. 24 * 60 * 60 * 60 jiffies + 1 equals the stated 5184001.
Couldn't you just do an if then then else loop looking at the value for TI?
I think that's what I show as the second technique?
@@8_Bit omg you noticed me lol. Yeah i realized that shortly after I posted it. And yet it still wasn't a perfect solution
Goddammit time traveling programmers, always covering up their tracks.
YOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOÒOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
Per favore ingranndusci di piu l immagine a sinistra
Goodness gracious. So many responses here, and nobody mentions the modulus operators, which is how you deal with overflowing integers. I'm sorry to say, but just about everybody here would not get past a basic programming interview at a company.
The c64 has no modulus operator. ( though it can obviously be implemented easily enough in assembly or in basic )
10 TI$="000000"
20 IF TI$
Close, it should be 20 IF TI
@@8_Bit It will work with 20 IF TI$
@@jjeeeekk I thought TI$
It's a little inconvenient to get the time limit value into the proper format for the comparison. You could write it as
T=60:T$=STR$(T):T$=RIGHT$("00000"+RIGHT$(T$,LEN(T$)-1),6)
@@jjeeeekk Building on your code I would guess, T=60 : T$="00000"+STR$(INT( T/60 )) : T$=RIGHT$(T$,6)
Meh, i don’t care what patrons want.
I renamed T as half my brain confuses it with TI but it is good to bring along both halves.
10 PRINT TI
20 W=TI+60
25 REM "WRAP W BUT LEAVE ENOUGH TIME TO CATCH TI>W @50"
30 IF W>=5183995 THEN W=W-5183995
35 REM "WAIT FOR TI TO WRAP IF W WRAPPED"
40 IF TI>=5183935 AND TI>W THEN 40
45 REM "SIMPLE NOW WRAPPABLE MOMENTS HAVE PASSED"
50 IF W>TI THEN 50
60 PRINT TI
In the last example which seems the optimal method, seems we should be able to fix throwing time off, by just storing TI before start and setting TI$ to correct value after the wait.