- PROGRESS
- What is PBNET?
- What does PBNET do?
- What did it take to create PBNET?
- How does it work?
- Project goals and limitations
- Throughput
- Protocol support and features
- Future plans / items under consideration
- Using PBNET
- FAQ
- Acknowledgments
- Packet encoding and decoding: done
- Rewritten enc_block, dec_block and pkt_checksum to ASM: done
- Respond to ICMP echo: done
- Configuration file support on PB-2000C: rewritten to keep settings within the library file (it's RAM storage): done
- UDP sockets: done
- DNS resolver (IN A only): done
- Ping program - no IP stack is complete without one: done
- Emulated serial port via TCP to Piotr Piatek's PB-2000C emulator: done, see https://github.com/wowczarek/pb2000em
- Compiled library so far: 9.8kB. Still too much and no TCP yet, but more to be rewritten in asm.
Current state: library + net.exe
tool - ICMP echo responder, DNS query, ping, configuration:
CAL
-> run net [ icmp | ns <host> | ping <host> [count] [size] [interval] | set < defaults | <key> <value> > ]
YouTube demo:
DL-Pascal is great, but the machine code it produces is bloated, plus it's Pascal. Only way to reduce binary size is to write more in assembly:
- What in C would be shift operators, in DL-Pascal are functions, meaning that there is loading of parameters and several jumps, and then finally a loop(!), because HD61700 does not support arbitrary bit shifts - only 4-bit shifts (
diX
) and 1-bit shifts (biX
) - Mathematical expressions produce lots of machine code. For example:
a := a + 5
produces 7 bytes more thaninc(a,5)
, and even at that,inc()
is a procedure (as per Pascal). - Function call overhead is noticeable, and so is the machine code increase from declaring functions (~30 bytes). Anything that is single-use, should be inlined. An example is the checksum function which itself is about 50 bytes of machine code, including loading of arguments, where declaring it and calling it uses more machine code than it probably would if it was fully inlined
- A DL-Pascal cross-compiler would be the best way to go if we wanted to optimise things - or at least initially a macro processor that would speed up certain operations and produce less machine code. Shifts, swaps, increments and such should be asm macros
PBNET is an attempt at creating something resembling a semi working IP stack for the Casio PB-2000 personal computer. It consists of an uplink daemon running on Linux, written in C, and a library / API / utilities for the PB-2000, written in DL-Pascal.
Casio PB-2000 (PB-2000C / AI-1000) is an 8-bit programmable pocket computer made and sold by Casio in the late 1980s, sporting a QWERTY + numeric keyboard, 192x32 dot matrix LCD and up to 64 kB RAM (32 kB + RP-33 RAM pack). The RAM is shared between system area, heap, stack and file storage, and is battery-backed. The PB-2000 has a calculator mode and a menu-driven file manager. The CPU is a custom Hitachi HD61700, same as used in another pocket Casio computer, the PB-1000.
What makes the PB-2000C unique is that it supports a number of programming environments, unlike most similar devices of this era that out of the box only supported BASIC. The PB-2000 has a built-in ROM with a K&R C (yes, C!) interpreter - that's the PB-2000C - or one with LISP (really) - that's the AI-1000 that was only sold in Japan. But that's not where it ends - it also has a ROM card slot, and ROM cards were available with BASIC (OM-53B) and Prolog (OM-52P). C was also available as a ROM card for the AI model. The PB-2000 can interface with the rest of the world via the FA-7 interface (RS-232 serial, parallel port, tape audio in/out) or the MD-100, which is the latter, but replaces the tape interface with a 3.5" floppy disk drive.
Unlike other 8-bits you might have heard of like Commodore, Atari , Spectrum and such like, it does not have a TV output and no fancy scan line juggling circuitry (it does have an LCD controller that supports sprites though), nor does it have a sound chip - only a simple piezo buzzer out of which you can PWM some chirps. But it's pocketable and runs on batteries. It was meant for engineering and academic use.
In this document I will mostly be referring to the computer as PB-2000 or simply PB, which may mean either the PB-2000C or the AI-1000.
Shut your trap! It's a pocket 8-bit!1!!!1!
Why, DL-Pascal of course! None of the ROMs available for the PB-2000 were compilers, even the C one is an interpreter, so, fast is what they are not. There is only one exception, and at the same time the only third-party ROM ever made for the PB-2000, which is DL-Pascal that was developed in 1989-1990 by Hans Larsson of Data-Larsson, Sweden (http://www.datalarsson.se/)
DL-Pascal is a proper, fully featured Pascal compiler, mostly compatible with Turbo Pascal 5.5 (no Object Pascal though) and QuickPascal 1.0, which supports inline assembly, has the familiar CRT unit, supports building new units (libraries), preprocessor, etc. DL-Pascal takes the PB-2000 to where Casio were never able to. The 64 kB ROM contains the compiler, a full screen editor with search/replace/cut/copy/move/goto capabilities, a menu-driven file manager, a debugger, a calculator / CLI mode with batch file support (basic "shell scripting") and more. Compiled binaries are small and code runs fast (see note below), and Pascal provides many features that other supported ROMs never did. DL-Pascal also came with a 300+ page grimoire of a manual.
Note on "fast" and "small binaries": DL-Pascal definitely produces fastest code out of any development environment supported on the PB platform just by virtue of being a compiler, however with the compiler being so tiny, sacrifices had to be made. There is little room for compiler optimisation, so compared to raw assembly, it is almost a waste of resources, however the convenience beats everything else available for the PB-2000C / AI-1000.
Well, yes and no. If you're even reading this, chances are that you already have one, but otherwise, the Casio PB-1000 and PB-2000 are sought after by collectors, although can still semi regularly be found on eBay for a few hundred (enter currency here). The hardware is is one part.
Now, as to DL-Pascal... If you have the DL-Pascal ROM, then congratulations, you are one of the chosen few. Firstly, knowledge of the history of DL-Pascal is very scarce. Most of what is known, comes from a handful (like, two maybe) of collectors on the French-speaking forum silicium.org. From there and from e-mail exchanges with a handful of people I have learned that only several hundreds of this ROM were made, if that, and even fewer have resurfaced and are in people's hands - again, mostly collectors. Apparently DL-Pascal was also sold with a modified PB-2000C where it replaced the built-in ROM. DL-Pascal is truly a rarity in the world of computing. I managed to get hold of a pre-release ROM via eBay a few years ago - apparently a one-of-a-kind card, but I got access to the proper production version (1.2) in mid-2021. Because the author of DL-Pascal, mr. Hans Larsson, opposes people sharing the dump of this ROM for reasons that are not completely clear - or at least this is what is being said on forums - DL-Pascal remains mostly unseen and unknown. Even a Google search for "DL-Pascal" returns only a few results.
The PB-2000 and me go a long way. When I was about seven or eight years old, a friend of the family sold his PB-2000C to my mother, and so began my adventure with computing. It came with the BASIC and Prolog ROMs, FA-7 and the FP-100 4-colour X-Y plotter. I used it throughout most of elementary school and secondary school and it's provided a huge help, mostly with maths, physics and chemistry, but also a great way to pass dull moments, because I wrote lots of games for it. Unfortunately in about 2001 I had it stolen, and so it never got into university with me, but it would have been proud knowing that I went for CS. In about 2010 I bought one again and never did much with it, then in 2016 I bought the DL-Pascal ROM, and finally in 2021 I started to use it again.
I want to say it's an IP stack, but I can only say this with some caveats. It is a half-duplex, synchronous, bi-directional packet pusher. It is a library and some applications that together allow the PB-2000C to talk UDP and TCP to the Internet, resolve DNS names and send and respond to ICMP echo requests.
Apart from having the hardware and DL-Pascal, not that much, but I developed lots of things in the process to make this work easier. I started in 2021 (I think), with hardware only and little debugging capabilities, because Piotr PiÄ…tek's PB-2000C emulator had no serial port support. Then I had to learn the HD61700 assembly and add DL-Pascal's inline()
format to Blue's HD61700 assembler, get it to work in Linux and write a barebones preprocessor and minifier so .asm files can be compiled and included where needed from within a Makefile
and comments are stripped from sources, and to write a simple file transfer script that delays bytes when sending over serial port. Copying data over serial is slow, and compilation is slow like a bastard on real hardware, so then then I added serial port support to Piotr's emulator and some remote control capabilities so I could compile easier. I am also working on a MD-100 FDD emulator that works as a file server right from a subdirectory in Linux, rather than on a disk image. Altogether I have a comfortable development environment now.
PBNET connects a Linux host (gateway, router, whatever) to the PB-2000 using its serial port, and allows it to exchange packets. The host side effectively handles framing and flow control ("Layer 2"), whereas the PB-2000 handles that, plus the actual IP packets. All of the processing is done entirely in the PB-2000, with the following exceptions:
- IP, ICMP, UDP and TCP checksums can be (optionally!) offloaded to the host side. The algorithm is dead-simple, but it adds even more delay, so this can be taken care of by the host
- If the PB-2000 is disconnected or otherwise unreachable, such as not running PBNET, the host can send ICMP unreachables on its behalf
The natural choice for a project like this was to use SLIP, which is a well-established standard, and that is what I tested initially. SLIP is trivial to implement, but it quickly turned out it wasn't going to work. SLIP requires hardware flow control in both directions, because it does not escape XON/XOFF. Unfortunately PB-2000C does not support hardware flow control in the RX direction, so a dedicated protocol was required that either worked with XON/XOFF and escaped those bytes, or did its own flow control. Because the PB-2000 is quite fragile when it comes to its serial buffer overflowing, flow control was required in the upper layers and because XON/XOFF is typically transparent, I had to do my own flow control.
I developed a simple block transfer protocol with explicit flow control, let's call it IPBSP for the lack of a better name: IP over Broken Serial Ports:
Data is transferred in up to n-byte blocks and the maximum is 224 bytes. The RX buffer is 256 bytes, but when it is filled up to more than 256-32=224 bytes, flow control triggers (XOFF), apparently even when not configured - at least in my version of DL-Pascal. [NOTE: double check with DL-Pascal 1.2]
The receipt of every block is confirmed with an ACK
byte and the last block in a packet (which can be less than n bytes), ends with a SEP
byte. Flow control is performed using two more bytes: STX
(stop transmission) and RTX
(resume transmission). So we have four control bytes.
Because the block size necessarily has to be controlled, we cannot afford an escape type encoding like the one used by SLIP, where a reserved byte XXX is transmitted as a pair: ESC + ESC_XXX - so we could have a portion encoded in anywhere between n and n ** 2 bytes. I opted for a variant of the escapeless encoding scheme by Ivan Korsarev, also see a good description here. When I first came across this, I couldn't believe that someone like Claude Shannon hadn't come up with this idea in, like, the 1940s - I really need to do more research here, I still refuse to accept that this isn't older than me!
Problem statement: transmit data using an alphabet A consisting of a symbols (256 for ASCII), over a medium supporting a reduced alphabet A' with a' symbols (a subset of A), which either lacks x symbols (here byte values), or they are otherwise unavailable or reserved. In our case a is 256, x is 4, a' is 252. To reduce A to A', we inspect a block of maximum length a minus x for x non-occurring bytes. Logically, a block of up to 252 bytes cannot contain all 256 values. We find x non-occuring bytes in the block and replace any of the reserved bytes with one of those. Then we send those x replacements before a block of data. This ensures a fixed maximum block size, at the cost of x (here 4) bytes per block, and extra processing - see goals and limitations. The decoding side reads this four-byte preamble and replaces any of the four replacements in the block that follows, with the four known reserved bytes. The end.
The encoder / decoder implementation handles two special cases. One: if there were no reserved bytes in the block, the first two bytes (replacements) in the header are equal - the decoding side will recognise this and copy the block as-is. Two: last 4 ASCII characters do not occur - we readily use 0xfcfdfeff
as replacements.
No acknowledgments are performed per block transmitted by the PB towards the host like we do in the other direction, only one ACK
per whole packet. With data volumes being so low, there is near-zero chance that the host will not be able to read all data coming from the PB in time. Corruption is possible, but then this is what checksums and retries are for. If data corruption proves a problem, this will need to be revisited, and perhaps per-block checksums or parity bytes will need to be implemented.
There are numerous other projects for 8-bit computers that include an Ethernet adapter and provide an IP stack, but they often require either modifying the hardware or are based on add-on boards that say go into the ROM card slot. PBNET allows for connecting an unmodified PB-2000, as is, with a suitable serial interface, to the Internet.
On the PB-2000 side, there is no state really, everything happens on demand:
-
When
pkt_get()
is called, the loop waiting for packets starts by sending theRTX
byte, so the host knows it can forward a packet to PB, and while waiting for packets, PB responds to anACK
byte with anotherACK
(but this clears the RX buffer on the PB side). This way, the pbnet daemon on the host and any application using PBNET on the PB-2000 can be started in any order. -
When
pkt_send()
is called, the PB-2000 just encodes and sends the whole packet block by block, ending with aSEP
byte, and then awaits anACK
from the host to confirm that the packet was received.
Start state | Trigger / Event | Action / conditions / comments | End state |
---|---|---|---|
!= READY | [HOST-->PACKET-->PB] | Queue packet or send ICMP unreachable | NO CHANGE |
->IDLE | Initial state | NO CHANGE | |
IDLE | [PB--RTX-->HOST] | PB ready to receive | ->READY |
READY | [PB--STX-->HOST] | Stop accepting packets from host | ->WRTX |
READY | [HOST--PACKET-->PB] | [TX BLOCK #1] | ->WACK |
WACK | [PB--ACK-->HOST] | [TX NEXT BLOCK...SEP] | ->WACK |
WACK | [PB--ACK-->HOST] | [NO MORE BLOCKS] | ->WRTX |
WRTX | [PB--RTX-->HOST] | PB ready to receive | ->READY |
IDLE | [PB--DATA-->HOST] | ->WBLOCK | |
WBLOCK | [PB--DATA-->HOST] | Restart WBLOCK timeout timer | ->WBLOCK |
WBLOCK | [PB--SEP-->HOST] | [HOST--ACK-->PB], Forward packe | ->IDLE |
WACK | TIMEOUT | ACK timeout | ->IDLE |
WRTX | TIMEOUT | RTX timeout | ->IDLE |
WBLOCK | TIMEOUT | Block receipt timeout | ->IDLE |
A DEAD
state exists for any failures while transmitting and receiving, but its mechanic has not been implemented yet. This is meant to wait and then attempt to reopen the serial port and TUN interface before attempting to go into IDLE again.
-
PBNET requires DL-Pascal. I admit that this severely limits the reach of this project. DL-Pascal however is the only environment available for the PB-2000 that allows building libraries and linking with them. Significant parts of PBNET are either already written in assembly or will be rewritten into assembly, but the interface is still via Pascal functions. Yes, I could write an all-assembly blob that someone could use to write applications for other ROMs than DL-Pascal, but this means mostly single-use applications. The goal is to provide a network API, not one application, but I may review this in the future since the majority of PB-2000C/AI-1000 owners only have the plain C/LISP ROM or BASIC. Even if you need to bundle a ton of machine code with your project, I suppose that is still an API - but what's the use if you have to write it all in assembly...
-
PBNET is a clean slate project. It is nothing more than one guy's attempt to see what he can remember and put together from a set of classic RFCs. PBNET is not based on, nor is it influenced by, any existing minimal IP stack such as uIP/LwIP, etc. I intentionally restrained myself from looking at those, but I have worked with the BSD sockets API extensively in the past, so some references are inevitable.
-
PBNET will only support the absolute working minimum. It does not aim for RFC compliance and working with every guideline that an IP stack should conform to. While I would be glad to implement all this, storage is limited and I have to cheat. On the PB-2000, PBNET will mostly silently drop packets when they are too big, fragmented, or otherwise not expected. This is actually not unusual today with firewalls everywhere, still it could be better.
-
PBNET does not support IP fragments. It could, of course, but RAM is severely limited. PBNET ignores fragments and always sets the DF bit in outgoing packets.
-
PBNET will do as much as possible in the PB-2000. We could move most of processing and connection handling to the host side and have it serve as a proxy, e.g. the PB would send a request like "create UDP socket to a.b.c.d port n", and the host would then do that and send back an ID, and then if data arrived, it would only send the payload, prepended with the ID and length. TCP could be completely outsourced to the host as well - but that is not what we want. One thing that does make sense to do is to proxy SSL connections via the host, as a native SSL implementation would be a serious overkill.
-
PBNET is slow to respond. That depends what we mean by "slow", however on the PB-2000 side, everything is written in DL-Pascal + assembly. While this is the best performance you can get on this platform with a high-level language, latency is significant. Every system call can add tens of milliseconds. Also the sheer slowness of the interface (9600 bps max) contributes to this. Overall, looking at test results, it seems like I am achieving at least 75% of RX/TX data rates achievable with no processing at all. See throughput.
-
PBNET works with the help of a host computer, not with two PB-2000s directly. Other than for fun purposes, there isn't much value in talking between two PB-2000s. You can still do it via the host - it is by all mean possible to have IP between two PBs, but this would mean flow control in the TX direction as well, which will make it slower. Maybe I will revisit this.
-
Initially PBNET will only support IPv4, which seemed fitting given the era when the PB-2000 was made. There are no technical limitations to supporting IPv6, just that it would be slower than IPv4. An IPv4 header is 20 bytes where an IPv6 header is twice that, so that difference alone would already consume 10% of the achievable bandwidth.
-
PBNET uses a dedicated half-duplex framing protocol, so only one side transmits at a time. Unfortunately it has to be this way. The PB-2000 does not support hardware flow control on the RX side, so that already precludes the use of SLIP, and because we send arbitrary binary data, XON/XOFF is also a no-go. On top of this, PBNET is being developed using an FTDI-based serial interface replacing the FA-7 or MD-100, and there is no control over buffering and no transparent flow control, so flow control has to be explicit and must be performed on the application side - for example, when we have received a full packet, we must immediately instruct the other end to stop transmitting until we will have handled it - and, possibly, finished sending a response - only then we resume transmission. In fact, the host suspends further transmission immediately after it has sent the SEP byte. While the packet is being processed, the serial port RX buffer could easily overflow.
-
PBNET is synchronous, so only one operation happens at a time. Unfortunately there is no way to call user-supplied ISRs (Interrupt Service Routines) without modifying either the hardware or the ROM, so we cannot jump and process packets when some data is received on the serial port while the PB-2000 is doing something else. The only way to operate is to wait for a packet, decide what to do with it, and send a response back if needed - or the other way round: send a packet, and wait for a response and only a response to that packet. Although we can have callbacks and simultaneous socket listeners, still only one thing happens at a time.
-
The host side uses a very shallow transmit queue, by default limited to 5 packets. This prevents from queuing endlessly just to have the PB-2000 respond after tens of seconds.
-
There is no support for receiving multicast. You can transmit multicast without issues, but for it to leave your host, you need multicast routing. "Why would you need multicast?!" is not the right question here - multicast could be needed for things like IoT integration/control or something else. It's just the fact that we would either need to support IGMP (and then probably PIM on the host), or statically subscribe to selected multicast groups via the host, listen on the host and forward to PB. Doable, but it's extra code and I'm not sure if it's worth the effort.
With the core routines (block encoding, decoding and checksums) written in HD61700 assembly, processing is already not the limiting factor and the framing overhead is low (4 bytes per max 224 bytes, plus SEP). Piotr Piatek's USB serial module is speed-limited on the RX side and maximum rates PBNET achieves are about 250 Bps RX + ~530 Bps TX. With the FTDI chip buffering, we don't have to worry about the speed at which we write to PB's port, but for use with Casio serial interfaces, PBNET uses staggered transmit and a fixed delay is added per byte (-l
option, microseconds, default is 1000 = 1 ms). This does not affect the performance via the USB interface, but allows uninterrupted transmission with the FA-7 or MD-100. This was tested by Piotr Piatek with an MD-100 and rates were 400+ Bps RX, 700+ Bps TX. With Piotr's USB module, we achieve ~250 Bps RX (limited by the module), and ~517 Bps TX (was ~530, but dropped since the PB now waits for an ACK after sending a packet). At this stage, optimising the encoding and decoding routines further, yields little improvement, as the majority of delays are on the serial port side - both hardware and RX/TX code in the ROM.
PBNET supports (will support) ICMP, UDP and TCP, and has a very basic DNS resolver (see below). A simple TFTP server and a net
tool (resolver, ping, show config) will probably be developed before TCP.
Notes on DNS resolver: The resolver is non-recursive, only handles IN A records (no reverse lookups, just returns back the IP address), and does not cache the results. Maintaining cache lifetime is difficult if you run the app/library on demand without a reliable real-time clock and with only three timers available to the user in DL-Pascal and how they are implemented. Best thing we could do is cache a result for n calls rather than n seconds.
- Initially PBNET will operate on single connections, one at a time, but the intention is to implement an event loop where multiple sockets can be handled using callbacks
- PBNET could eventually support IPv6 - it's only a matter of parsing more types of packets
- PBNET could be ported to other ROMs than DL-Pascal, but this would mostly lead to single-purpose applications - only DL-Pascal supports reusable, loadable libraries on the PB-2000 platform.
- A serial port and a null-modem cable, or Piotr Piatek's USB adapter (http://www.pisi.com.pl/piotr433/usb100_e.htm)
- Linux or WSL, anything with a serial port connected to your PB-2000C, a C compiler
- The host daemon uses POSIX calls and Linux' TUN API, but it should be easily portable to BSD and other POSIX compatible OSes
- There is nothing wrong with Windows anymore really and a native Windows port is of course possible, but this is not the author's priority
- VIP ticket: The DL-Pascal ROM card
- Storage is very limited, so it is recommended to have the RP-33 expansion installed to have a total of 64 kB of RAM
- PBNet uses very little stack space and uses less than the default 2 kB of heap/var space TODO: exact figures
- Either an MD-100 or FA-7 interface with a null-modem cable (DB25 on the PB side, likely DB9 on the host side), or the USB adapter
- Download the source on a Linux host, type
make
. This will build the host daemon,host/pbnet
, build thehd61700
assembler, compile the assembly files, include inline code and build all the resulting sources into./pb2000/build
. - Copy files from
./pb2000/build
onto the PB-2000 however you like. There is a script includedpb2000/sendfile.sh
which cleans up Pascal sources and sends files to a serial device with optional speed limit in Bps (bytes per second), which translates to a per-byte delay. With an MD-100 or FA-7, you shouldn't need a speed limit or delay, but if you getRead Fault
while loading the file using MENU -> load -> rs232c, slow it down. Note: theRead Fault
s I encountered were most likely due to a bug in my DL-Pascal pre-release. - Go into CAL mode, type
do build.bat
. This will build thePBNET.DLE
library,PBNET.UNI
unit file and thenet.exe
tool. Warning:build.bat
cleans up the sources after the build, including removing itself. If you want to leave the sources intact, rundo build.bat noclean
. - To compile anything against PBNET, both
PBNET.UNI
andPBNET.DLE
are required, but once compiled,PBNET.UNI
can be deleted.
Note on filename case: Like most humans, I prefer lower case. Although the do
and run
commands are not case sensitive, compiling and linking requires the unit files and library files to be ALL CAPS. This is why the libraries are all uppercase, but extra tools like net.exe
are lowercase.
The default settings are normally enough for stable operation and best performance, but can be adjusted if needed - especially IP addressing and the DNS server, say if you have a local one.
The defaults are: baud rate 9600, block size 224, IP address 10.99.99.2
and DNS server 9.9.9.9
(https://quad9.net/). There is no gateway setting - this is a point to point link and the PB only has one possible route.
To configure PBNET / change any defaults, use the net.exe
tool with run net set <key> <value>
, or run net set defaults
to reset to defaults. The key and value are passed directly to the pbn_set()
function, so the same can be done within your own program.
Both net set ...
and pbn_set()
methods accept the following keys / values:
ip <a.b.c.d>
: PB's IP address
rate <1..7>
: baud rate, corresponds to the baud rate code used throughout Casio HD61700 platforms, 1 = 75 bps, 7 = 9600 bps
dns <e.f.g.h>
: IP address of your DNS resolver - only one
blocksize <16..224>
: best to leave at default value (224), also must match on the host side!
ttl <1..255>
: IP Time To Live (hop limit) set in packets generated by the PB. Default is 128
checksums <tTyYfFnN>
: enable or disable checksum calculation and validation on the PB, default is true. [not fully implemented yet, compute ICMP checksum but we set UDP checksum to 0]
Note 1: pbn_set()
really looks for the first letter of the keys, so you can shorten them, this is also why baud rate is 'rate' not 'baud'.
Note 2: Since the PB-2000C uses a RAM based filesystem, settings are store within the PBNET.DLE library. We store in a dummy procedure as DB assembly data, but we also store the defaults.
Just run pbnet -h
to see the available options and defaults. Using command line parameters you can configure the serial device (or IP address/port of the emulator), baud rate, block size, per-byte delay (set to zero when connected to emulator), IP address and netmask of the host side, etc. The default netmask is /24 rather than say /30 which makes more sense for a point to point interface, but this is mostly to test and demonstrate the PB discarding packets meant for other addresses on the same subnet. The default IP address on the host side is 10.99.99.1
.
To build your own program with PBNET, you need the PBNET.UNI
unit file and PBNET.DLE
library file. Then it's only a matter of specifying uses pbnet;
in your Pascal code. Although units come with a "main" initialiser function that is called when the library is loaded, PBNET omits that and pbnet_init
has to be called explicitly and none of the API calls will work unless it is.
On the host side, just run the pbnet
binary with correct arguments.
PBNET uses a combined ippkt
record which uses Pascal's variants (like C's unions) to hold all protocol headers and payload. The case
statements are just DL-Pascal's way to declare variants.
The packet record looks as follows:
type
ippkt=record
len:word; { total length of packet }
l4_len:word; { length of layer 4 payload:icmp, udp, tcp, etc. }
case :boolean of
false:{ variant 1:raw packet buffer } (data:array [0..1499] of byte);
true: { variant 2:IPv4 header } (
ip:ip_hdr;
case :byte of
0:{ variant 1:Raw IPv4 payload } (payload:array [0..1479] of byte);
1:{ variant 2:ICMP + payload } (icmp: icmp_hdr);
2:{ variant 3:UDP + payload } (udp: udp_hdr);
3:{ variant 4:TCP + payload } (tcp: tcp_hdr)
)
end;
This record is of a fixed size of 1500 bytes total (plus len
and l4_len
fields). Normally, only one ippkt
variable is required for all operations. The l4_len
field contains the payload length for the given protocol (ICMP, UDP, TCP) and is set automatically on packet receipt, and on transmission, this field is used to indicate the payload size we want to send with the given protocol. A pointer to TCP payload will probably added and will be set according to data offset.
Protocol headers are defined as follows:
const
{ IP protocol numbers }
ipr_zero=0; ipr_icmp=1; ipr_udp =17; ipr_tcp =6;
{ header lengths }
ip_hlen=20; icmp_hlen=8; udp_hlen=8; tcp_hlen=20;
type
quad=array [0..3] of byte;
ipaddr=quad;
{ IPv4 header }
ip_hdr=record
ver_ihl:byte;
dscp_ecn:byte;
len:word;
id:word;
fl_foff:word;
ttl:byte;
proto:byte;
csum:word;
src:ipaddr;
dst:ipaddr;
end;
{ ICMP header + payload }
icmp_hdr=record
mtype:byte;
mcode:byte;
csum:word;
id:word; { belongs to ICMP echo, just here for convenience }
seq:word; { belongs to ICMP echo }
payload:array [0..1471] of byte;
end;
{ UDP header + payload }
udp_hdr=record
sport:word;
dport:word;
len:word;
csum:word;
payload:array [0..1471] of byte;
end;
{ TCP header + payload }
tcp_hdr=record
sport:word;
dport:word;
seqno: quad;
ackno: quad;
doff:byte; { 5 without options, 6 with MSS }
flags:byte;
wsize:word;
csum:word;
uptr:word;
payload:array[0..1459] of byte;
end;
The payload (here using a variable pkt
for illustration), can be accessed as:
pkt.data
: raw packet (IP payload + IPv4 header)pkt.ip.payload
: IPv4 payloadpkt.udp.payload
: UDP payloadpkt.icmp.payload
: ICMP payloadpkt.tcp.payload
: TCP payload
The socket is defined as follows:
{tcp state}
tcp_state=(none,listen,synsent,synrecv,estab,finwait1,finwait2,closewait,closing,lastack,timewait,closed);
{ a socket descriptor }
socket=record
state:tcp_state; { state (for TCP) }
lport:word; { local port:source port for PB->world, destination port for world->PB }
rport:word; { remore port}
remote:ipaddr; { remote address }
end;
Instead of source and destination (port, address) we look at remote and local.
Extra constants / special variables are:
const
port_any=0;
var
addr_any:ipaddr=(0,0,0,0);
These are used to accept any remote source and any remote port. For example with UDP, if we set sock.rport:=port_any
and sock.remote:=addr_any
and pass this socket to udp_recv
, it will accept the first UDP packet destined to our IP address and our sock.lport
, and set rport
and remote
to the remote source port and IPv4 address, so from this point, further udp_recv
calls will filter this and only this connection.
Function / procedure | Usage |
---|---|
function strerr(e:shortint): string; |
Error code to string. |
function ipaddr_parse(var s:string;var a:ipaddr):boolean; |
Parse an IP address from string. False on failure. |
function ipaddr_str(var a:ipaddr):string; |
Convert an ipaddr to string. Always succeeds... |
function quad_eq(var qa:array of byte;var qb:array of byte): boolean; |
Returns true if two 4-byte sequences are equal (IPv4 addresses basically) |
procedure pkt_hold; |
Instruct the host to stop forwarding packets. |
procedure pkt_ready; |
Instruct the host to start forwarding packets. Call some receive function immediately after this. |
procedure pkt_prime(var pkt:ippkt); |
Set IP version, ID and other basic fields. Necessary for a new packet - also increments IP ID on every call. |
function pkt_get(var pkt:ippkt;timeout:word):shortint; |
Grab the next incoming IP packet, whatever it is. |
function pkt_send(var pkt:ippkt;timeout:word):shortint; |
Transmit an IP packet. |
function echo_respond(var pkt:ippkt; timeout:word):shortint; |
Checks if pkt is an ICMP echo request and responds to it. |
function udp_recv(var pkt:ippkt;var sock:socket;timeout:word):shortint; |
Receives the next packet that fits the sock - see source or "Program flow" below for details |
function udp_send(var pkt:ippkt;var sock:socket;timeout:word):shortint; |
Sends a packet to what is in sock . Make sure to set pkt.l4_len to match your pkt.udp.payload - see "Program flow". |
function udp_respond(var pkt:ippkt;timeout:word):shortint; |
Bounce the pkt back to where it came from. |
function tcp_connect(var pkt:ippkt;var sock:socket;timeout:word):shortint; |
I've been hearing this chap Vint Cerf is working on this new protocol. I need to give him a call. Not implemented yet. |
function dns_resolve(shost:string;var rip:ipaddr;var pkt:ippkt;timeout: word; retr: byte):shortint; |
Ask your DNS server for an IN A of shost and store results in rip . If shost is an IP address, no query is sent - we don't support in-addr.arpa. |
procedure pbn_shutdown; |
pkt_hold followed by closing the serial port. |
function pbn_init:boolean; |
Open the serial port, set random IP ID and randomize . |
function pbn_set(sk: string; v: string):boolean; |
Set a setting programatically. See "Configuring" above. |
procedure pbn_defaults; |
Set all settings to defaults. |
Most PBNET calls return an error code. E_OK = 0
is returned on success, negative values E_XXX
are returned on failure and positive values are reserved for protocol-specific errors, such as DNS RCODE response codes.
Code | Value | Meaning |
---|---|---|
E_OK |
0 | Success |
E_TMO |
-1 | Timeout (timeouts are specified in tenths of seconds) |
E_INTR |
-2 | Call interrupted (any key pressed - TODO: only interrupt on BRK key) |
E_TRUNC |
-3 | Packet truncated: there is a mismatch between the IP length field and actual length of packet received |
E_CRC |
-4 | Incorrect checksum (either IP header or ICMP/UDP/TCP checksum) |
E_MTU |
-5 | Packet too big |
E_UNXP |
-6 | Unexpected packet received - we get this if we want to receive packets from a specific host and/or port, but received something else |
E_ERR |
-7 | Any other error |
E_INIT |
-8 | PBNET uninitialised |
E_ARG |
-9 | Argument error |
E_NOMINE |
-10 | Packet not for this socket. As seen with UDP: we are receiving from a specific host to a specific port, but a new "connection" arrived |
dns_resolve
, follows DNS RCODE on top of the above. The following ones are defined - others are still returned if they happen:
const
{ DNS return codes (RCODE) }
de_formerr=1;de_servfail=2;de_nxdomain=3;de_notimp=4;de_refused=5;de_notzone=9;
Internal error codes are negative, zero is E_OK
, DNS RCODEs are positive, hence shortint
signed type.
The function function strerr(e:shortint):string
can be used to convert error codes to strings.
Note: neither the library nor the interface declare a single ippkt
variable. The user application is meant to provide one, and we can happily work with a single instance of ippkt
.
var pkt: ippkt;
- you need a packet to work with.
pbn_init;
- will return true if opening the serial port succeeded. At this not much more happens than opening the serial port - we are not going to receive anything until we are ready.
pkt_prime(pkt);
- this will prime your packet with basic fields, and also increment the IP ID field. Unless you really care about distinct IP IDs, you can call this once.
Then do whatever you wish with the packet. If it's a TCP, UDP or ICMP packet, set the l4_len
field to your payload length. If it's a raw IP packet, set l4_len
to zero and set len
(not ip.len
) to the correct total length (including the 20-byte IP header). Chances are you won't be doing too much of this, so in most cases, l4_len
does it.
pkt_send(pkt, timeout);
- this will transmit your carefully crafted packet. If any L4 checksum is non-zero (TCP,UDP,ICMP), it will be computed for you. This still does not allow the host to send you packets just yet.
pkt_ready;
- this will send RTX to the host, telling it it can start transmitting anything that might have been queued up. You better try receiving something immediately after this.
pkt_get(pkt, timeout);
- this will grab the next incoming IP packet with which you are free to do as you wish. This will also call pkt_hold
behind the scenes, so host will not forward anything again until you call pkt_ready
.
var sock: socket;
- declare a "socket" - it's really only a convenient holder for source/destination ports and addresses.
sock.dst := whatever;
- you can resolve a hostname into sock.dst
or whatever, or set four octets manually, or initialise the whole sock
in the var
section.
sock.lport := whatever;
- set the source port to a value of your choice, or set zero to be assigned a random ephemeral port.
sock.rport := whatever;
- set your destination port.
udp_send(pkt,socket,timeout);
- send your packet. This will take care of the source/dest IP and port, but set l4_len
to your payload length.
var sock: socket;
- declare a "socket" - it's really only a convenient holder for source/destination ports and addresses.
sock.dst := whatever;
- you can resolve a hostname into sock.dst
or whatever, or set four octets manually, or initialise the whole sock
in the var
section.
sock.lport := whatever;
- set the listening port. If set to zero, the first UDP packet will be accepted and the socket will become bound to this conversation.
sock.rport := whatever;
- set it to zero to accept the first packet to your local port. When that happens, your socket is bound to this remote port and udp_recv
will return e_nomine
if it reached the right port on your end, but is part of a different conversation (another source address or port).
udp_recv(pkt,socket,timeout);
- receive next packet that fits the socket.
udp_respond(pkt,timeout);
- populate udp.payload
with whatever, set l4_len
and this will swap the sources / destinations and send the packet. Ideal for a single-packet request/response.
The above behaviour (binding remote addr/port) contradicts the connectionless nature of UDP. This is really meant for convenience, nothing else. With how limited we are, we will probably be handling one thing at once, and if we get e_nomine
we may ignore it or return an error code in whatever protocol we are handling. This will not be an issue anymore if/when I decide to implement an array of sockets.
Note: socket fields are stored in the native byte order, no need to swap.
pbn_shutdown
- this will close the serial port and call pkt_hold
before doing so.
- Your IP packet variable is reused between different calls (well, unless you have lots of RAM - do you?), so make sure you set
l4_len
and anything else you want to set before sending a new one, because you are working with recycled data. - All timeouts are specified in DL-Pascal's timer units. Manual says they operate at an interval of "about" 10 Hz. It is more likely tied to the LCD periodic pulse that runs at 55 Hz, so I assume they are decremented every 5th run. I need to do some more decompiling to verify.
pkt_send()
explicitly denies sending to our own address and sending packets with the same source and destination. Just saying.
-
Q: Why DL-Pascal? Hardly anyone can access this thing!
-
A: Well, tough. Let's hope by the time this project is semi-complete, there will be some movement on that front. Pascal is a high-level language and allows one to design an actual API where it's trivial to write networked applications. It is also much easier to prototype things before or instead of writing them in assembly. I stand by this decision, even if eventually this becomes asm-only.
-
Q: Why only a single
socket
? -
A: It's all down to RAM. We could easily have an array of them and indicate which one received data. Can do, but later.
-
Q: I have everything connected and running, the host is receiving packets from the PB, but I am getting no response from the DNS server or any other Internet or LAN host other than the host connected to the PB - why?
-
A: PBNET forwards packets from your PB towards your host, but that is where it ends. The host needs to have IP forwarding enabled, your router must have a route to your PB via your host and if required, needs to allow NAT from your PB's IP address, or otherwise your host needs to NAT PB's address to its own. Configuring these things is up to you, the user.
Like pretty much every project relating to PB-1000 and PB-2000C, I would like to thank Piotr Piatek (http://www.pisi.com.pl/piotr433/index.htm) for his insights into the hardware and help with testing. This man has created an amazing body of work around these platforms and he also developed FPGA/CPLD based hardware modules for serial communication and mass storage, replacing the bulky FA-7 or MD-100 - he also developed an emulator for the PB-2000 that I use for compiling PBNET without having to transfer the source back and forth. I would also like to thank Pascal a.k.a. Xerxes for pushing his pre-production DL-Pascal card to eBay one day, Juergen Keller for his help with accessing DL-Pascal 1.2 and Blue for the HD61700 cross-assembler (http://hd61700.yukimizake.net/)