Lock iconRectangle 1Rectangle 2 + Rectangle 2 CopyShapeRectangle 1

Packing and Unpacking I/O Bytes

 Click HERE to download a printable copy of Chapter 8


Packing and Unpacking I/O Bytes

Before we proceed with more involved application examples, it is important that we explore one area of software where a little further explanation is required before the procedures become clear. This is the packing and unpacking of I/O bytes. Fundamentally, with the C/MRI we always “talk” to the railroad through the I/O cards. These cards can be the separate cards plugged into an I/O motherboard or the “I/O cards” built into the SMINI.

Each I/O card type, input or output, supports either 24- or 32-I/O lines that are grouped together in sets of eight lines each. We call each group of 8-lines a port. The ports are labeled A, B and C for 24-line cards and A, B, C and D for 32-line cards. Each line within a port has two states typically referred to as being either high or low, on or off, 1 or 0, +5Vdc or 0 and so forth.

In computer-ese we often speak of an I/O line as a “binary bit” or simply “bit” for short. Eight bits, when grouped together as they are within each port, are referred to as a “byte.” Therefore, each port is equivalent to 1 byte (of data), made up of 8 individual bits.


Using an output card, we write to, or set, the values on each output line. This way the software controls devices on the railroad. With an input card we read the value that the railroad has set on each line. This way the software monitors the status of devices on the railroad.

The software needs to be able to look at each of the wired-lines on each port as a bit. Like the wire itself, each software bit can assume two states typically referred to as being either high or low, on or off, 1 or 0, etc. Each byte has 8 bits and they exhibit a one-to-one correspondence to the 8 lines for each port. Software, when it reads and writes to an I/O card’s port, must treat the grouping of the 8-bits for each port as a single entity.

Software must read from each input card a byte at a time. Software must write to each output card a byte at a time. Neither QuickBASIC nor Visual Basic, nor most other programming languages, read or write at the individual line, or bit level. However, most software variables within our programs correspond to only a few hardware lines. For example, if SE(12) represents a 3-LED signal at the east end of Block 12, it corresponds to 3 output lines, i.e. one line for each of the 3 LEDs.

Additionally, let’s assume that SW(9) is a two-head 5-LED signal containing green, yellow and red LEDs on the upper head and yellow and red LEDs on the lower head. Thus SW(9) corresponds to 5 output lines. A typical application for this configured signal would be at the facing point end of a turnout leading into a CTC controlled passing siding.

To provide separate or independent control, each LED is wired to a separate output pin. Per the above, SE(12) needs 3 lines and SW(9) needs 5 lines. What is needed, is a simple procedure whereby we can position the SE(12) bits to use the lower 3 bits of a port while the SW(9) bits are aligned to use the upper 5 bits of a port. This procedure is called Packing. That is, we pack the bits representing different software output variables into a whole byte prior to transmitting the byte out to the railroad.

When we read an input byte from the railroad we need a similar type of procedure to separate the various bits representing the different input devices. This procedure is called Unpacking. For example, each signal lever on a dispatcher’s CTC panel requires 2 input lines corresponding to 2 input bits. Thus we could group 4 sets of signal lever inputs on a given input port. The unpacking operation would be used to separate the different signal levers into separate software variables such as SL54, SL56, SL60 and SL62 denoting the 4 signal levers.

Fig. 8-1 is a flow chart, showing our real-time loop expanded to include the unpacking of input bytes and the packing of output bytes. It is possible to write subroutines to do the packing and unpacking for us and I did that with my original Heathkit computer using assembly language. However, invoking subroutines takes extra set up effort and slows down the real-time loop. Also, after extensive study, I find it is just as easy to accomplish the packing and unpacking operations using direct in-line code as it is to define a number of required variables before invoking the subroutine. I find this to be especially true when it takes but one line of code to pack, or unpack, each railroad device.

Fig. 8-1. Real-time loop including unpacking and packing of I/O bytes

In actuality, using direct in-line code is especially attractive because it only takes one line of code to pack or to unpack each software variable. Also, once you become familiar with it, packing and unpacking are very easy steps to implement. The procedure is always the same. Do it a few times on a couple of different I/O cards and you will become an expert to help others with their implementation.


Every one of our C/MRI programs requiring packing and unpacking will use two sets of constants defined by the two programming lines listed in Fig. 8-2.

B0 = 1: B1 = 2: B2 = 4: B3 = 8: B4 = 16: B5 = 32: B6 = 64: B7 = 128

W1 = 1: W2 = 3: W3 = 7: W4 = 15: W5 = 31: W6 = 63: W7 = 127

Fig. 8-2. Packing and unpacking constants

It is important to note that: the statements in Fig. 8-2 stay exactly the same and are included in every program where we need to pack and unpack I/O bytes.

I refer to the first line of constants as the “B” or Bit position constants. They are used to position, or shift, each variable right or left so that each variable lines up with the desired bit positions within the input and output bytes. The constants in the second line are referred to as the “W” or Width constants and are used to define the width of each variable, i.e. how many bits wide need to be unpacked from the input byte.

For readers desiring an understanding of why we pick these specific constants and how they are used, I will explain the mathematics of packing and unpacking. However if you would rather just go ahead and dive right into unpacking and packing operations, please feel free to skip ahead to the section Unpacking Procedure and then skip ahead to Packing Procedure.


In almost every C/MRI application program we make use of integer mathematics – that is mathematics dealing with whole numbers only including 0, 1, 2, 3, 4, … and so forth. BASIC, as well as other languages, can easily handle fractional numbers such as 3¾ or 3.75 but in our railroading applications there is little need to use this capability.

Using only integer operations helps keep our C/MRI applications simple. To force QuickBASIC to treat everything as integers we can invoke the define integer statement DEFINT A-Z at the beginning of our programs. This tells the compiler to treat all program variables beginning with A through Z as integer variables. Because all program variables must begin with an alphabetical character, this statement defines all variables as integers. 

Integer mathematics truncates all fractions. Thus 7 divided by 3 becomes 2 and not a 2.5. A 19 divided by 6 becomes 3 and not 3.16666. An 8 divided by 3 becomes 2 not 2.6666. From the latter example it is important to stress the point that: integer mathematics does not round up or down to the nearest whole number – integer mathematics simply truncates all fractions!

However, it turns out that integer division is exactly what we need for most of our C/MRI application programming. This is because we use division to shift a number, in our case an input byte, to the right so that we can easily sort out various bit groupings. Let’s see how this works.

In the decimal number system, dividing a given number by powers-of-ten shifts the decimal point to the left. For example taking the decimal number 17925316 and dividing it by different power-of-ten we have the relationships shown in Table 8-1.

Table 8-1. Dividing a decimal number (e.g. 17925316) by powers-of-ten

Power-of-ten  n

Value of 10n

Regular divide = 17925316 / 10n

Integer divide = 17925316 \ 10n

































In Table 8-1 the regular forward slash (/) indicates a regular, or real number, divide – i.e. where you deal with fractional numbers. The backward slash (\) indicates an integer divide – i.e. where the fraction is truncated. These two types of divide are standard with QuickBASIC and Visual Basic and in many other languages.

Looking at Table 8-1 we see that using the regular divide by powers-of-ten simply shifts the decimal point to the left by whatever power-of-ten we use. For example using n = 2, the decimal point is shifted 2 places to the left. Using n = 3, the decimal point is shifted 3 places to the left and using n = 6 yields 6 places to the left. Moving the decimal point to the left is equivalent to shifting the number to the right as is shown in the integer divide column where the fraction is truncated.

The same thing happens when we use binary mathematics to separate input bytes into different bit combinations. The only difference is that when working with binary numbers, the 1s and 0s in our input bytes, we need to use powers-of-two rather than powers-of-ten. Table 8-2 illustrates the situation for integer dividing an example input byte of 10110010 by different powers-of-two.

Table 8-2. Dividing an input byte (e.g. 10110010) by powers-of-two

Power-of-two  n

Integer divide = 10110010 \ 2n

Decimal equivalent for power-of-two = 2n

Symbol used for bit position constant

































Looking at Table 8-2 we see that integer division by powers-of-two shifts the binary bits within our input byte to the right by whatever power-of-two we use. For example, using n = 2, the input byte is shifted 2 places to the right. Using n = 3, the input byte is shifted 3 places to the right and using n = 6 yields 6 places to the right. In digital logic, such an operation is referred to as a right-shift and the bits that get shifted out to the right, which is beyond the binary point (equivalent to the decimal point), are truncated i.e. they fall into the “bit-bucket” and are lost.

In our programming, we like to work in decimal rather than in binary, so we simply use the decimal equivalent for the powers-of-two as indicated in Table 8-2. For each of these powers-of-two, I have assigned a constant symbol denoted as B0, B1, B2, … up to B7. These bit position constants are exactly those we established in the first statement line back in Fig. 8-2; i.e. B0 = 1, B1 = 2, B2 = 4, B3 = 8 and so forth up to B7 = 128.

Thus, by dividing each input byte by an appropriate bit position constant, we can shift any input byte to the right however many times we need to align any railroad device to the rightmost bit positions. Once this is accomplished we simply need to retain the appropriate number of the rightmost bits, equivalent to the number of bits wide required by the railroad device, while zeroing out the leftmost bits. Mathematically this is accomplished by logic ANDing the shifted byte with the appropriate width constant defined in Table 8-3.

Table 8-3. Width constant definition

Symbols used for width constants

Decimal value of constant

Binary value of constant






















Logic ANDing two bytes is accomplished by separately ANDing each bit position within the two bytes using the relationship illustrated in Fig. 8-3.

Fig. 8-3. Logic AND operation

The output of a logic AND operation for each bit position is always 0 unless both input bits are 1. Thus, if a given railroad device, such as a 5-light signal, is ANDed with W5 (which is a 00011111) the 5 signal bits that are ANDed with the 11111 are preserved. The remaining 3 bits that are ANDed with the 000 are masked away, becoming zeros in the result. In a like manner W1 is used when we need to strip off 1 bit, W2 is used to strip off 2 bits, W3 to strip off 3 bits and so forth up to using W7 to strip off 7 bits.

Fundamentally the AND operation, combined with an appropriate width constant directly converts all the bits that are not related to the railroad device to zeros and leaves the bits that are directly associated with the device undisturbed.

Thus to unpack any railroad device such as an occupancy detector, pushbutton, signal lever or whatever else is desired from its corresponding input byte we perform two steps:

  1. Use the bit position constant with an integer divide to shift the corresponding input byte so that the railroad device of interest is in the rightmost bit positions.
  2. Use the width constant to mask off all but the desired bits representing the railroad device of interest.

It is really more difficult to explain in general terms than it is to actually perform the operations. Therefore let’s simply dive into formulating the unpacking procedure.


Typically the first operation within the real-time loop is to read the railroad inputs and then unpack the various railroad devices from their respective input bytes. Unpacking is required because when the C/MRI software reads a port it brings in all eight bits, i.e. all 8-lines, as one entity, namely the input byte, IB(n) where the ‘n’ corresponds to the port number.

After reading each byte, we need to separate the various bit groupings to form the separately needed software variables like BK(1), BK(2), … for block occupancy detectors and TF(18), TF(19), … for turnout position feedback, and so forth. This separating of the various railroad devices is accomplished by the unpacking procedure. Implementing the unpacking procedure requires dividing by the appropriate bit position constant and then ANDing with an appropriate width constant.

Unpacking inputs requires one statement per railroad device and the format is always identical:

 Device = IB(n) \ By AND Wz

To convert the above general format to a specific statement for a given railroad device simply substitute for Device the selected variable name for the railroad device. Then substitute for “n” the port (or byte) number to which the device is connected, for “y” the starting bit position within the byte and for “z” how many input lines wide is the device.

For example if an occupancy detector for Block 15 is wired to bit Position 6 on the third port of the first input card, then the corresponding unpacking line would read:

BK(15) = IB(3) \ B6 AND W1

The railroad device is BK(15). The IB(3) denotes the port number to which the occupancy detector for Block 15 is attached. The B6 constant denotes that the occupancy detector BK(15) is connected to bit Position 6. The width constant W1 is used because the occupancy detector connection uses only 1 wire, i.e. its connection is only 1 bit wide.

As another example, let’s assume we have a CTC panel signal lever, denoted as SLEV62 and connected to Bits 2 and 3 of the same port. Two bits, or line wires, are used because, as we will see when we cover CTC panels, a signal lever uses 2 wires as inputs. The corresponding unpacking line would read:

SLEV62 = IB(3) \ B2 AND W2

We keep the same input byte, IB(3), because the signal lever is connected to the same input port. The bit position constant is changed to B2 the “starting bit” location for the wiring connections to Bits 2 and 3. The width constant W2 is used because the signal lever input is 2 bits wide.

****Important point****

Every unpacking statement must use the back-slash (\) integer divide. If you accidentally substitute a forward-slash, the program’s input variables will become jumbled. Thus, if you run into the situation where your input variables appear to be all messed up, make sure that you are using the correct back-slash in all your unpacking instructions.


Once unpacking is mastered, it is relatively straightforward to understanding packing. For those interested, I will explain the theory behind packing operations. If you would rather skip the theory and dive right into packing, then simply jump ahead to the next section titled Packing Procedure.

Packing output bytes is much like the inverse of unpacking input bytes. We need to shift the bits in each railroad output variable so that they line up with their required positions corresponding to the lines where each device is connected. Then we fold its bits into the output byte being formulated without disturbing the bit positions used by other devices sharing the same output byte. Let’s dissect this paragraph a little to see what it really means.

To explain what is required for packing, it is easiest to simply work through an example. Assume we have two different color-light signals (i.e. signals using separate LEDs for each color) connected to Card 0 Port B as illustrated in Fig. 8-4.

Fig. 8-4. Example signal connections to demonstrate packing requirements

One is a single-head 3-LED signal, which I have given the variable name SIG(9), and the other is a double-head 5-LED signal denoted as SIG(14). Also, assume that software has set SIG(9) to yellow, decimal 2, and SIG(14) to green over red, decimal 17, as indicated by their corresponding bit pattern diagrams shown in the lower right portion of Fig. 8-4.

All we need to do to create the output byte for the example situation is:

  1. Copy the bit pattern for SIG(9) directly into the output byte.
  2. Shift the bit pattern for SIG(14) three places to the left and then insert it into the output byte without disturbing the bit pattern already established for SIG(9).

Implementing step 1 is easy, we simply use the statement:

OB(2) = SIG(9)

Now how do we accomplish step 2? We saw earlier, in Table 8-2, that dividing by powers-of-two shifted a bit pattern to the right. Similarly, multiplying by powers-of-two shifts a bit pattern to the left. Therefore, we can perform the required left shift – to line up our SIG(14) bits to their desired output byte positions – by simply multiplying by the appropriate bit position constant selected from the same set of constants we used for unpacking.

In this example, variable SIG(14) needs to be shifted so that its least significant bit (LSB) starts at, or aligns with, bit Position 3 of the output byte.  To accomplish this we simply need to multiply by bit position constant B3. The result of this multiply shifts SIG(14) three bits to the left, as illustrated in the lower right portion of Fig. 8-4. This lines up the SIG(14) bits so that they are ready to be inserted into the output byte. Note that the multiply operation zeroes the bit positions vacated by the shift operation.

The trick now is to insert the shifted SIG(14) bits into the output byte without disturbing the least significant bit positions already set up for the other devices sharing the same output byte – in this case the bits for SIG(9). This is accomplished by logic ORing the shifted SIG(14) byte with the output byte being formulated, i.e. already containing SIG(9). The combination multiplying and ORing operation, i.e. step 2, is written as the statement:

OB(2) = SIG(14) * B3 OR OB(2)

Logic ORing does the job for us because logic ORing two bytes is accomplished by separately ORing each bit position within the two bytes using the relationship illustrated in Fig. 8-4. The output of a logic OR operation for each bit position equals 0 only when both inputs are 0 otherwise the output is a 1. That is, if input A is 1 OR if input B is 1 then the output C is 1.

Logic ORing a 1 or a 0 with a constant Logic 0 does not change the state of the 1 or 0. Because the shifted byte always has zeros in the bit positions used by previously packed variables their corresponding bits remain unaltered during the OR operation.

Similarly, because the bit positions not yet filled in the output byte are always zero, the ORing operation effectively copies the shifted byte into the output byte being formulated without disturbing the already filled bits. Fig. 8-4 illustrates the operations for the SIG(9) and SIG(14) example. The corresponding two statements to make all this happen are simply:

OB(2) = SIG(9)

OB(2) = SIG(14) * B3 OR OB(2)

To generalize the procedure for packing all railroad devices such as signals, switch motor controls, panel LEDs or whatever else you want to pack into their corresponding output bytes we need to:

  1. Directly copy the first device into the output byte. By first device I mean the railroad device wired to the least significant bits within a port.
  2. For each subsequent device connected to the port, multiply the device name by its appropriate bit position constant to shift the device into position and then OR it into the output byte.
  3. Repeat step 2 for each additional device attached to the port, i.e. until all devices are loaded into the output byte.

This three step process is repeated for each output byte. Once all the output bytes are packed, the complete output array OB(1), OB(2), OB(3), … up through OB(NO) are ready to be transmitted to the interface. Note that variable NO is the number of output bytes within a given node.

As with unpacking, it is really more difficult to explain in general terms than it is to actually perform the operations. Therefore let’s simply dive into formulating the packing procedure.


Typically the last operations within the real-time loop are to pack the various railroad devices into their corresponding output bytes and then send them out to the railroad. As with unpacking, packing is required because when the C/MRI software writes to a port it sends all eight bits, i.e. all 8-lines, as one entity, namely the output byte, OB(n) where “n” corresponds to the port number.

Therefore, before we can write out the output bytes we need to combine the various bit groupings representing the railroad outputs, for example SE(1), SE521, SW(14), SW231, … for trackside signals and SM(7), SM(42), SM23, …for switch motor control and so forth. This combining of the various railroad devices to form the output bytes is accomplished by the packing procedure. 

Packing the first device into each output byte is directly accomplished using the relationship:

OB(n) = Device

Then, each additional device within a port is packed using the relationship:

OB(n) = Device * By OR OB(n)

To convert the above general formats to specific statements for a given railroad device simply substitute for Device the selected variable name for the railroad device. Then substitute for “n” the port (or byte) number to which the device is connected and for “y” the device’s starting bit position within the byte. Note that the width constants are not used for packing.

Packing, however, does recursively use the output byte. By this I mean we pack one device into the output byte and then we use that intermediate value of the output byte to pack in the next device. Then we use that intermediate value of the output byte to pack in the next device. For each output byte we repeatedly keep that process going forward until we have packed in all the desired devices.

For example, if switch motor SM(2) is wired to bit positions 0 and 1 on Port 5, 3-LED trackside signal SE(7) is wired to bit positions 2, 3 and 4 on the same port, and 3-LED signal SW(11) is wired to bit positions 5, 6 and 7 of the same port, then the corresponding packing statements are:

  OB(5) = SM(2)                'Place switch motor 2 in output byte 5

  OB(5) = SE(7) * B2 OR OB(5)  'Fold Signal East 7 into output byte 5

  OB(5) = SW(11) * B5 OR OB(5) 'Complete output byte 5 by inserting ...                                               '. . . Signal West 11                                                                                     

The Subscript 5 is used in the output byte OB( ) because the three railroad devices are connected to Port 5 which for an SMINI corresponds to its “Card 1” Port B location. The 3 railroad devices connected to this port are SM(2), SE(7) and SW(11). Bit position constant B2 is used when folding in SE(7) because the wiring of SE(7) starts at the port’s Bit 2 position. Constant B5 is used when folding in SW(11) because the wiring of SW(11) starts at the port’s Bit 5 position.

A quick way of checking that you have the correct bit position constants is to “walk through” each of the connected devices. Switch Motor 2 being a 2-wire device uses up B0 and B1. Signal East 7, being a 3-wire device, must then start at B2 to use up B2, B3 and B4. Signal West 11, being a 3-wire device, must then start at B5 to use up B5, B6 and B7. The port is completely filled and SM(2) starts at B0, SE(7) at B2 and SW(11) at B5.

That completes our discussion covering the packing and unpacking procedures I now recommend. Once you set up the required instructions for a couple of your actual I/O ports, I am confident that you will find that writing unpacking and packing statements becomes a very straightforward operation you will perform essentially by rote.

However, before we continue forward generating actual railroad application examples, I want to spend just a moment explaining the difference between the packing procedures I now recommend and what you may have read from my previous publications.


Most if not all of my publications prior to the Version 3.0 of the C/MRI User’s Manual recommended a slightly different packing procedure. This previous procedure recommended that an output byte be first initialized to zero and then using the same statement for packing the first device as well as the subsequent devices. Following this procedure results in the packing statement formats:

OB(x) = 0

OB(x) = Device * By OR OB(x)

In this situation, the format on the second line is used for each device within the port, i.e. also including the first device. Using the examples from above, the generated statements are:

  OB(5) = 0                    'Initializes port to zero

  OB(5) = SM(2) * B0 OR OB(5)  'Places switch motor 2 in output byte 5

  OB(5) = SE(7) * B2 OR OB(5)  'Folds signal east 7 into output byte 5

  OB(5) = SW(11) * B5 OR OB(5) 'Completes output byte 5 by folding in ...

                                  '. . .signal west 11                                                             

Although the above code is not as abbreviated as the now recommended set of four statements, the end results are identical. In fact, with some thought, it is easy to prove they are identical. First note from Fig. 8-2 that bit position constant B0 equals one and multiplying anything by 1 does not change anything. Thus it is easy to drop the * B0 term. Secondly considering that ORing any byte with a byte of zeros does not change anything. Thus it is easy to drop the OR OB(5) portion of the code in Statement 2 above. This leaves the second statement as OB(5) = SM(2) which is exactly what we wrote for the packing procedure now recommended. Doing this the initializing of OB(5) = 0 is irrelevant because the next statement simply overwrites the initialization to zero by setting OB(5) = SM(2).

Because the newly recommended approach simply defines OB(5) = SM(2), automatically placing zeros into all the bit positions not used by SM(2), there is no need to initialize OB(5) = 0 when using the newly recommended procedures.

However, when using the previously recommended procedure it was essential to initialize each output byte to zero. Otherwise the contents of the being formulated output byte would be contaminated by the content of the previous value of the output byte.


As a final summary point in this chapter, always use the back-slash (\) in your unpacking operations along with the bit position constant to shift the input byte to the right. The back-slash signifies to QuickBASIC and to Visual Basic, as well as to many other languages, that you want an integer divide.

If you should ever need a regular divide use the forward-slash (/). This way 5/2 comes out to be a 2.5. With the integer divide 5\2 comes out to be a 2 rather than 2.5. The result of an integer divide is the same as with regular division except that the decimal fraction is dropped.

I have used the integer divide, performed using a backslash, and the logic AND and OR operations for both simplicity and speed of execution. The procedures should work fine for 99+ percent of user applications. In fact, I have tested the procedures for every version of Basic that I could get a hold of including BasicA, GW Basic, QuickBASIC and Visual Basic on many different computers.

The only combination I found that would not work is on the very old Apple II series of machines when used with either Applesoft BASIC or Apple Integer BASIC. I doubt if any C/MRI user is still using such outdated machines and software. However, special packing and unpacking software procedures for this case are defined on pages 301 and 302 of the book Build Your Own Universal Computer Interface – Second Edition. It is also covered on page 216 of Build Your Own Universal Computer Interface – First Edition. This effectively outdated software is not included in this User’s Manual.

Please do not be concerned at this point if you still feel a little confused about unpacking and packing operations. As I said before, they are harder to explain in general than they are to actually implement.

Believe me, with the first couple of I/O ports under your belt, the unpacking and packing portion of I/O programming becomes an operation performed by rote and a breeze to implement. With this in mind, let’s move forward with detailed railroad examples covering signaling and turnout control with our SMINI.