Search the Community
Showing results for tags 'p-system'.
-
Anyone here who use/used the p-system and programmed the 99/4A in the Pascal language?
-
Anyone who has ever written an assembly program using the Editor/Assembler package knows the drill. Use the editor to create the source file. Save it as MYFILE:S Run the assembler to convert MYFILE:S to a codefile MYFILE:O. Use the Load and Run to execute the code file. When creating a Pascal program with the p-system, you can do almost exactly the same. The procedure is then, in detail, like this: Press E to start the editor, followed by ENTER to start a new program. Use the editor to type in the source code for your program. Quit the editor, then Write to a filename. Type in myfile and ENTER to save the file. Press Exit to leave the editor. Now press C to compile the program. Type in myfile as the source to compile and $ to create the codefile with the same name. When the compiler has finished, press eXecute to run your program. Enter myfile and it will run your new program. As you can see, this mimics the Editor/Assembler workflow almost exactly. The p-system has another card up its sleeve, though. It's called the workfile. As the name hints, it's the file you are working with. Assuming you don't already have a workfile, the same task as I described above will now run like this: Press E to start the editor, followed by ENTER to start a new program. Use the editor to type in the source code for your program. Quit the editor, then Update to create the workfile and leave the editor. Press Run to execute your program. That's it. The p-system will check if your latest workfile has been compiled. In this case it has not, so it will compile it for you, and then execute it. The next time you select Run, the program will run immediately, as it's already compiled. It should be obvious that the workfile concept saves several keystrokes, when you are creating your program. If you run it and find you want to edit something, you simply invoke the editor and it will open the workfile for you, no questions asked. So, what's the drawback? Well, as long as you are working on one program, and that program fits in one single source file, then none, really. To start a new program, you have to go to the Filer and Save the current workfile under some filename, like myfile. Then you can create a New workfile (also with the filer), exit it and open the editor to enter the text into your new workfile. While in the Filer, you can also check What your current workfile is, and Get any existing file to become the workfile. However, the features inherent in the p-system, some of which I've described in earlier posts, does invite you to create substantial projects. Such projects frequently consist of a main program file, a file to include in the main program file, a file with the source for a separately compiled unit and a file with assembly support for either the unit or the main program. When you have several files like that, the maneuvers to get the correct file to become your current workfile, each time you want to change something, aren't worth it. There's no automation of assembling a source file either. Personally, I never use the workfile. But if you are new with the system, then it's likely that you start by creating software where it works well. A note about filenames. When using the Editor/Assembler, most users adapt some method to tell the difference between the source and object code. They don't have the same file format, so you can tell from that what's what, but you can still not have the same file name for both. My method was usually to name them TEST:S and TEST:O or something similar. With the p-system, although you normally use ten character file names there too, they are really 15 characters long. When you tell the Editor to create the file myfile, it will actually save the file as myfile.text. And when you tell the compiler to compile myfile to the same filename, it will actually compile myfile.text to myfile.code. That's why you can seemingly use the same filename for both the source and the code. They aren't the same, actually. If you did use the workfile in the example above, and then wanted to save it as myfile, you would open the Filer and tell it to save the workfile to myfile. It will then report that it has copied *SYSTEM.WRK.TEXT to MYFILE.TEXT and *SYSTEM.WRK.CODE to MYFILE.CODE.
-
If you haven't read the threads about memory management for code and data in the p-system, using Pascal, then you should, prior to reading this post. After that, you'll understand that the p-system supports pretty complex software. When writing larger programs, you frequently find that you want to re-use functions you've already written before. To begin with, the p-system already from the beginning contains some such commonly used functions, which you can simply reference in your own program, and then use them just like if you have written the code yourself. In the p-system, this allows you to access some functions that are specific for the 99/4A, like sound processing, sprite handling and character pattern definitions. The standard library To manage this in a simple way, the p-system supports a library function. Programs can be written to be included in a library, which you then can use, even though they aren't inside your own program. The p-system contains a file called *SYSTEM.LIBRARY. Inside that file, which resides on the system disk (*file means a file on the system disk), are the supporting units supplied with the system. The SYSTEM.LIBRARY file contains several units from start. Assume you want to place an asterisk at the center of the screen. This program will do that. program display1; begin gotoxy(20,12); write(chr(42)); end. Now assuming you want a different shape, one that's not available among the standard characters, then you want to redefine the character. The 99/4A does allow for that, but the standard Pascal language has no support for such machine specific things. Here the predefined library function set_pattern comes to resuce. It will do the same things as CALL CHAR in BASIC. Consulting the compiler manual, we find that this function resides in the unit support in the system library. To get access to it is as simple as saying you want to use functions in that library, and then just use it as if it was built into Pascal. program display2; uses support; begin set_pattern(42,'0081422418244281'); gotoxy(20,12); write(chr(42)); end. When the compiler creates the code file from this source code, it will see that you want to use the functions available in the support unit. As you have not told the system where to find support, it will look for it in *SYSTEM.LIBRARY. It is there, so that will work. The compiler will then generate code to define that the routines referred to in support are located in the *SYSTEM.LIBRARY file. When execution comes to set_pattern, an external segment call is done. The operating system will know, from data in the code file, that it needs to load a code segment which is not inside the program running now, but in another file. The support unit will be loaded from the library into memory, where it can be executed. This involves accessing the disk where the library is residing. But all this happens automagically. The only thing needed is the single line uses support in your code. If you have more than one set_pattern call in your program, you'll not notice. Upon the second call, the unit support is already memory resident, so it's called just like a routine in your own program. As you can see, then handling is identical to that of internally declared segments in your own code, which I described in a previous thread. Indeed, as a minimum, each unit in a library is a separate segment. The *SYSTEM.LIBRARY file contains seven units from the beginning. Regardless of how many you use, they are only loaded into memory when needed. Look at this program, which uses two units. program display3; uses support, speech; type longstring = string[255]; procedure waitlong; var i: integer; begin for i:=1 to 32000 do; end; begin gotoxy(20,12); write(chr(42)); waitlong; set_pattern(42,'0081422418244281'); waitlong; say('Hello'); end. The program will first output the * symbol at the center of the screen. It will then spend several seconds in an empty loop. If you are loading the program from a physical diskette drive, the drive will have time to stop. After a while, the drive which contains the *SYSTEM.LIBRARY file will start up, since the program has reached the set_pattern call. The unit support is not in memory at that time, so it requires loading. You'll see the * change shape when this happens. The program will then wait again, long enough for the drive to stop once more. Suddenly, it will start again, when the program reaches the say statement. Unit speech is at this time not loaded, so the access to the library is redone. When it's ready, the computer will speak Hello. If you once again use eXecute to run this program, the sequence will repeat. If you instead use the User restart command, the program will run through the code without starting any diskette drive to load the library routines. User restart preserves the execution environment, incluing loaded internal and/or external segments, so the program can run from start to end without any need for external access of various supporting software. Already as-is, this is a powerful tool to make your programs able to do more things. But it doesn't end there. You can create your own library units, append them to the *SYSTEM.LIBRARY and/or create your own libraries, which can be used in combination with the system's library function. We'll look into that in follow-up posts in this thread.
-
In another post I described how simple it is to make more efficient use of the small memory available in a standard 99/4A. That post described the handling of data. Now let's look at the program itself. The code pool concept When the p-system is running, it uses all 48 K RAM (32 K CPU RAM and 16 K VDP RAM) for different purposes. VDP RAM is of course used to create the image to display, with character pattern table, display table etc. The remaining part of the VDP memory is used as the primary code pool. That's near 12 Kbytes or so. In the expansion memory, the 8 K part is used solely by the system. Here are the code for the parts of the operating system that can't run from the p-code card, system variables, the 80 column screen and such things. Yes, the p-system normally runs in text mode, 40 columns wide, but by default it simulates an 80 column screen, where you can move the viewport to the right or left, so you can see one half of the screen at each time. Thus by simply writing to the screen, you get these 80 columns simulated with no effort in your own program. The 24 K RAM part is used as the secondary code pool. Code is loaded here if the primary code pool is full, or if the code segment contains TMS 9900 machine code (assembly programs), as they can't execute from the video memory. This part of the memory is also used for the stack. In the post about the data memory I described some use of the stack for data memory allocation. The stack is also used for calulations, tracing subprogram calls and such things. The stack starts at the highest memory location (FFFFH) and grows downwards. At the other end of the 24 K RAM area, the heap is located. The heap is similar to the stack in that it grows and shrinks on demand, but it's different in how it operates. On the stack, new data is allocated on top of what's already there. When data is discarded, it's always removed from the top. On the heap, on the other hand, data is of course originally allocated in the same way. If you allocate ten items, they are added to the heap in that order. The only difference then is that it grows in the other direction, as it starts at A000H and grows upwards. The big difference is that you can remove things from the heap in any order. When you remove something that was created early, a "hole" will appear in the heap. The p-system IV.0 does implement a true heap, not just another stack that's called a heap, like some earlier p-systems did. Thus a hole can be reused, provided the new variable to allocate isn't larger than it can fit in the hole. The p-system can also garbage collect the heap, so it can be packed when needed. The alternate code pool resides in between the heap and the stack. The space available for it hence depends on what's going on in the system. Assuming your program fits in the primary code pool, it will not compete with the stack and heap for space in the 24 K memory area. But if it's larger than what fits in the primary code pool, or if it contains assembly support, then it must be loaded in the secondary pool. In the simplest of these cases, the primary pool is completely unused. That's of course not good. Now we should be aware that the operating system itself may load code in the code pools too, if it can't run that code from the p-code card directly. Fortunately, the PME (p-machine interpreter, the assembly code that executes the p-code) is flexible enough to be able to run p-code from CPU RAM, VDP RAM or even directly from GROM. The p-code on the card itself is stored in GROM chips, so in many cases it can run directly from there. That's the reason for why the 48 K TI 99/4A could run the p-system IV.0, where the Apple II, which also had 48 K RAM, but no p-code card with separate GROM, required an additional 16 K RAM card to be able to run the simpler first versions of the p-system, called Apple Pascal. Code segmentation But how do we make a larger program, that doesn't fit as it is in the primary code pool, to still fit in there, so we can use as much data memory as possible? The p-system's solution to this is called segments. In the p-system, a code segment is a piece of code that must be loaded into memory as a consecutive unit. When there are more than one code pool available, as in the 99/4A, it's an advantage if the largest segment you need to load is smaller than the smallest code pool available, since then the system can always place it where it's most efficient. In Pascal, you can create code segments in your own program, when there's is a benefit to do so. A classic reason is that you have code that runs to set things up before your main program really starts to run, but then never runs again. Or the opposite, code that runs only when the main program is about to stop, to save results in files or whatever. Another typical example can be code that runs only when you are printing results, or when you are inputting data. Have a look at this program. It's a stub that contains a number of different such functions. program doesitall1; type alldata = record (* All things needed *) end; var bigdata: alldata; level: integer; procedure initialize; begin (* Fixes all setup in the bigdata structure *) end; procedure cleanup; begin (* Saves what need to be saved in the bigdata structure *) end; function menu: integer; var temp: integer; begin repeat write('What to do? '); read(temp); until temp in [1..4]; menu := temp; end; procedure dataentry; begin (* Handles user input to the bigdata structure *) end; procedure calculate(complexity: integer); begin for i := 1 to complexity do (* Makes some complex calculations on bigdata *); end; procedure printservice; begin (* generates hardcopy of the things in bigdata *) end; being (* doesitall1 *) initialize; level := 3; repeat case menu of 1: dataentry; 2: calculate(level); 3: printservice; end; until menu=4; cleanup; end. When the main program starts, it begins by a setup procedure. Then it repeatedly calls menu to get the user's choice for what to do. The choice calls a corresponding procedure, until the user quits, in which case a cleanup is done before exiting. In a program like this, everything is loaded as one code segment. If it doesn't fit in the smaller main code pool, it has to compete with stack and heap space in the secondary code pool. Now, assuming it's big enough so that is a problem, how can we handle that? In many systems, like in Extended BASIC, it's possible to run one program from another, so you can overlay code in memory. But data is usually lost when you do it, so it has to be saved manually. In assembly, you can do everything, but that also means that simple things like the menu function above become complex. Forth has other means available, like forgetting definitions and loading other defintions from disk. But you have to manually control that, or define a subsystem that does. If you use Pascal with the p-system, the only thing you need to do is to consider which routines you don't always need. Here, it's pretty simple to see that this list of procedures have one thing in common: They are never needed simultaneously. Initalize Cleanup Dataentry Calculate Printservice Once we have figured that out, we only need to inform the system about it. You do that by telling the compiler which parts of the program can be separate segments in memory. program doesitall2; type alldata = record (* All things needed *) end; var bigdata: alldata; level: integer; segment procedure initialize; begin (* Fixes all setup in the bigdata structure *) end; segment procedure cleanup; begin (* Saves what need to be saved in the bigdata structure *) end; function menu: integer; var temp: integer; begin repeat write('What to do? '); read(temp); until temp in [1..4]; menu := temp; end; segment procedure dataentry; begin (* Handles user input to the bigdata structure *) end; segment procedure calculate(complexity: integer); begin for i := 1 to complexity do (* Makes some complex calculations on bigdata *); end; segment procedure printservice; begin (* generates hardcopy of the things in bigdata *) end; being (* doesitall2 *) initialize; level := 4; repeat case menu of 1: dataentry; 2: calculate(level); 3: printservice; end; until menu=4; cleanup; end. This is all it takes from you. The system will now load only the non-segment parts of the program doesitall2 when you execute it. Immediately, there will be a cross-segment call to initialize. But as a user, you need to do nothing. The operating system will check if segment initialize is in memory. It's not the first time, so it will automatically fetch it from disk and load into an available space in code pool. Then, as you enter your different choices from the menu system, the appropriate cross-segment calls will be made, and the system will load the code as needed. If you first run data entry, then maybe it fits at the same time as initialize remains in memory. If you then do a printout, that part perhaps also fits together with the already loaded segments. When examining the printout, you realize you should return to data entry. In this case, no loading of any segment is done, since the system finds that it's still there, since you used it last time. Now you call calculate. The segment fault (segment is not in memory), which triggers loading the segment, in this case causes a memory fault. The primay code pool is full with other segments, and there's not enough space between the code and the stack in the secondary pool to load the new segment. In this case, the system first attempts to free up more space in the code pool by optimizing the position of the already loaded code. P-code is dynamiclly relocatable, so the system will move code segments around, to make sure they are adjacent, so they occupy a minimal amount of memory. Then it will try to load the missing code segment again. If this faults once again, it will examine the list of currently loaded segments. The system keeps track of whether there is any open call to a segment. If so, it must remain in memory. This means that if one segment calls another, both must be in memory at the same time. But in this case, there are segments that aren't called. Like initialize. The system now checks the segments that are possible to remove. It keeps a simple call history, so the one that hasn't been used for the longest time is sacrificed. In this case, it will be initialize. Eventually, you select to terminate the program, in which case the cleanup segment is loaded. One or more other segments are then removed from memory, if it's necessary to make the cleanup segment fit. As you now hopefully understand, the p-system provides you with quite a complex (at least for its time) system for memory management. You can also see that using this powerful tool is comparatively simple, or even very simple, in your program. The next post on a similar topic will be about separate compilation. It's a very powerful tool, which combines all the memory management features I've described in the first two posts with an ability to write code that's both easier to manage, reuse and debug, in a format where you get all these benefits with very little extra work. There is a reason I've always claimed that the p-system is/was the most comprehensive software development environment for the TI 99/4A!
-
Today, there are several ways to get way more memory in the TI 99/4A than it had originally. But to use that memory, you have to write programs that manage it. Back in the days, when 32 K RAM was the only memory expansion available, you had to be careful with how you used memory. One way to make that easier was to use the p-system. It was originally developed with a few specific targets in mind. One of them was to be able to run in systems with small memories. Hence the system has a few available functions for that. Data memory If you create a global variable in Pascal, it will exist as long as the program runs. It will occupy memory, regardless of whether you use the variable or not. If it's an integer, using two bytes, that may not matter. But if it's a larger buffer, it's not a good idea. One way could be to declare the variable in a subprogram instead. Compare these two programs. Note that although they are syntactically correct, i.e. they can be compiled without errors, they don't work. Unimportant parts of the logic is just hinted at by comments. program version1; type buftype = packed array[0..1023] of char; mempointer = ^integer; var buffer: buftype; x,y: mempointer; procedure copier(a,b: mempointer); begin (* Use buffer to move data from place pointed to by a to position b *) end; begin (* version1 *) (* Assign addresses to x and y *) copier(x,y); (* Do more things *) end. Here is the other one. program version2; type buftype = packed array[0..1023] of char; mempointer = ^integer; var x,y: mempointer; procedure copier(a,b: mempointer); var buffer: buftype; begin (* Use buffer to move data from place pointed to by a to position b *) end; begin (* version2 *) (* Assign addresses to x and y *) copier(x,y); (* Do more things *) end. The only difference in these two examples is where the variable buffer is declared. But that makes a big difference. In version 1, the variable buffer will occupy one kilobyte of memory all the time. In the second version, on the other hand, buffer will only require that kilobyte as long as procedure copier is running. When it's not, the space is released and can be used for other things. The penalty is a little more time consumed at the call to and return from the subprogram copier. This is one example of how simple it is for the programmer to use the memory management available with Pascal and the p-system. Simply by moving the declaration you can control memory usage.