© Sridhar Anandakrishnan 2018
Sridhar AnandakrishnanPropeller Programminghttps://doi.org/10.1007/978-1-4842-3354-2_5

5. Compression in Spin

Sridhar Anandakrishnan1 
(1)
Department of Geosciences, University Park, Pennsylvania, USA
 

In this chapter, we will implement the compressor and decompressor purely in Spin using Test-Driven Development (TDD) methods. Rather than jumping straight into a PASM implementation, starting with a Spin version lets us ease into the problem and also debug more effectively. Once we have a working algorithm in Spin, we will translate it to PASM. In addition, a Spin version can act as a simulator to produce compressed data streams that the PASM decompressor will have to successfully operate on, and vice versa.

5.1 Structure of the Project

There will be two spin files: steim_spin0.spin and steim_spin0 Demo.spin . The file steim_spin0.spin is where the actual compressor and decompressor are implemented as a separate object. The file steim_spin0 Demo.spin is the driver file with the MAIN entry point into the code, as well as where the testing methods are defined. In addition, ensure that the file TestDrivenDevelopment.spin is in the same directory. Your directory should look like this:
../images/459910_1_En_5_Chapter/459910_1_En_5_Figa_HTML.jpg

5.2 Goals of This Chapter

In this chapter, we will write the Steim compressor and decompressor in Spin. Remember, we have an array of long samples (4 bytes each) called sampsBuf . The compressor will produce a byte array, packBuf, which contains the packed or compressed data. We want to do the following:
  • Start the process by copying 3 bytes from sampsBuf[0] to packBuf. This is done just once to initialize the compressor.

  • Take the difference of sampsBuf[1] - sampsBuf[0]. If that difference fits in 1 byte, then put that 1 byte in packBuf. If that difference is 2 bytes, put those 2 bytes in packBuf, and if that difference is 3 bytes, put those 3 bytes into packBuf.

  • Store a 2-bit code in comprCodeBuf that reflects which of three things we did.

  • Do the previous two steps repeatedly for all the samples (Figure 5-1 shows an example of a real-world “loop”).

../images/459910_1_En_5_Chapter/459910_1_En_5_Fig1_HTML.jpg
Figure 5-1

The Loop, Agony Point, Darjeeling Hill Railway, 1880. Photo uploaded by Bourne and Shepherd; artist unknown. Public domain.

5.3 First Iteration

Begin by copying the template spin_template.spin and saving it as steim_spin0_Demo.spin. Listing 5-1 is the driver program that sets up the Propeller and then runs the tests to exercise the code. Make the changes shown in the file.

 1  OBJ
 2    UARTS      : " FullDuplexSerial4portPlus_0v3 "
 3    NUM        : " Numbers "
 4    STEIM : " steim_spin0 "                  ' <<
 5    TDD : " TestDrivenDevelopment "          ' <<
 6
 7  CON
 8    NSAMPS_MAX = 128                         ' <<
 9
10  VAR
11    byte mainCogId, serialCogId
12    byte comprCogId                          ' <<
13    byte packBuf[NSAMPS_MAX<<2]              ' <<
14
15    long nsamps, ncompr                      ' <<
16    long sampsBuf[NSAMPS_MAX]                ' <<
17    long comprCodeBuf[NSAMPS_MAX>>4]         ' <<
18
19  PUB MAIN
20      mainCogId := cogid
21      LAUNCH_SERIAL_COG
22      PAUSE_MS(500)
23
24      UARTS.STR(DEBUG, string(CR, " Compression ", CR, LF))
25      UARTS.STR(DEBUG, string(" mainCogId: "))
26      UARTS.DEC(DEBUG, mainCogId)
27      UARTS.PUTC(DEBUG, CR)
28      UARTS.PUTC(DEBUG, LF)
29      STEIM.INIT(NSAMPS_MAX)                ' <<
30      TDD.INIT(DEBUG)                       ' <<
31      TEST_THAT_SAMP0_IS_PROPERLY_PACKED    ' <<
32      TDD.SUMMARIZE                         ' <<
33
34  PRI TEST_THAT_SAMP0_IS_PROPERLY_PACKED | t0, nc
35    nsamps := 1
36    sampsBuf[0] := $AB_CD_EF
37    nc := STEIM.COMPRESS(@sampsBuf, nsamps, @packBuf, @comprCodeBuf)
38    t0 := nc <> -1
39    t0 &= (packBuf[0] == sampsBuf[0] & $FF)
40    t0 &= (packBuf[1] == sampsBuf[0] >> 8 & $FF)
41    t0 &= (packBuf[2] == sampsBuf[0] >> 16 & $FF)
42    TDD.ASSERT_TRUTHY(t0, string("Test that samp0 is properly packed"))
Listing 5-1

steim_spin0_Demo : First Iteration of Implementing the Compression in Spin

  • Lines 15–17: Declare the arrays where the packed samples will be stored (packBuf), the samples themselves (sampsBuf), and where the compression codes will be stored (comprCodeBuf). I know that packBuf will be at most four times the length of NSAMPS_MAX and that comprCodeBuf will be 1/16th that length.

  • Lines 30–31: Initialize the STEIM and TDD objects.

  • Lines 32–33: Run the test and finish.

  • Lines 36–38: Perform the compression for a single sample.

  • Lines 39–42: Verify whether the output is as we expect.

  • Line 43: Perform the test and print the results.

Add all the lines marked with '<< and add the method TEST_THAT_SAMP0_IS_PROPERLY_PACKED. Lines 4–5 add the STEIM and TDD objects. From now on, any method in the file steim_spin.spin can be called by STEIM.METH_NAME. Similarly, any constant in that file is STEIM#CONSTANT_NAME. Lines 13–17 declare the new variables that will be needed by the compression and the new arrays where the uncompressed samples live before compression ( sampsBuf ) and where the compressed bytes live ( packBuf ).

Finally, in lines 30–31 the two objects are initialized, and in line 32, the first test is run. By convention, tests begin with the word TEST and are descriptive and grammatical. The test method itself is in lines 35–43. Finally, the SUMMARIZE method is called that prints out the number of tests run, the number that succeeded, and the number that failed.

The test is simple: set sample 0 to a known value, call the compression routine, and verify that the compressed data in packBuf is correct. The returned value from COMPRESS is the number of bytes in packBuf or -1 if there is an error; if there is no error, packBuf and comprCodeBuf are modified.

Listing 5-2 shows the code that does the actual work.

 1  CON
 2      CODE08 = %01
 3      CODE16 = %10
 4      CODE24 = %11
 5  VAR
 6      long _nsmax
 7
 8  PUB INIT(nsmax)
 9  " Set the max length of the input samples array
10      _nsmax := nsmax
11
12  PUB COMPRESS(psampsBuf, ns, ppackBuf, pcomprCodesBuf) : ncompr
13  " Inputs :
14  "   psampsBuf - address of sampsBuf array (long array).
15  "   ns - length of 'sampsBuf' (number of samps to compress).
16  "   ppackBuf - address of 'packBuf' array (byte array) where
17  "              samples are to be packed.
18  " pcomprCodesBuf - address of array of compresion codes
19  "                  (long array) - 16 codes per long.
20  " Output :
21  "   ncompr - length of packed array (bytes)
22      if ns == 0 'this isn't an error - but do nothing
23          return 0
24
25      if (ns < 0) | (ns > _nsmax)
26          return -1
27
28      ' handle sample0 first - it is always packed to
29      ' 3 bytes regardless of its size
30      bytemove(ppackBuf, psampsBuf, 3)
31      ncompr := 3
32      long[pcomprCodesBuf] := CODE24
33
34      if ns == 1
35          return ncompr
36      return -1
Listing 5-2

steim_spin0.spin : First Iteration of the Compression Code

  • Lines 1–4: Constants that define the compression codes.

  • Lines 8–10: The initialization routine. Save a local version of NSAMPS_MAX.

  • Lines 12–21: The compression method, along with comments detailing what the inputs and outputs are.

  • Lines 22–26: Check the inputs.

  • Lines 30–32: Perform the compression for the first sample.

  • Lines 34–35: Return in the case of N ≡ 1.

  • Line 36: Otherwise, return an error.

The actual compression is simple. We copy the three low bytes of sampsBuf[0] to packBuf[0]-packBuf[2], we set the compression code to reflect that this is a 3-byte packing, and we return the number 3. The calling code for COMPRESS is as follows:

nc := STEIM.COMPRESS(@sampsBuf, nsamps, @packBuf, @comprCodeBuf)

The method definition is as follows:

PUB COMPRESS(psampsBuf, ns, ppackBuf, pcomprCodesBuf) : ncompr

MAIN passes in the addresses of the arrays; in particular, it passes the memory location of the first element of each array (@sampsBuf). The method definition saves those addresses as psampsBuf, and so on.

5.4 Passing Arrays to Methods

The compression method in the STEIM object must access the arrays sampsBuf, packBuf, and comprCodeBuf. To do so, it needs the address in hub memory where each of them is stored. The calling method is written thusly:

nc := STEIM.COMPRESS(@sampsBuf, nsamps, @packBuf, @comprCodeBuf)

The symbol @sampsBuf is shorthand for “address of the first byte of the first sample of the sampsBuf array.”

The definition of the called method in the STEIM object is as follows:

PUB COMPRESS(psampsBuf, ns, ppackBuf, pcomprCodesBuf) : ncompr

Here the variable psampsBuf holds that address. To access that data, we must inform the compiler that the variable is an address (rather than a value). In other words, to access the i-th sample (sampsBuf[i]), we must say long[psampsBuf][i]. Similarly, ppackBuf holds the address of the byte array packBuf, and it must be accessed as byte[ppackBuf][i]. To copy data from sampsBuf to packBuf, we must do something like the following:

bytemove(ppackBuf, psampsBuf, 3)

Here, 3 bytes are copied from the address psampsBuf (which is the address of the start of that array) to the address ppackBuf (which is the address of the start of the packBuf array).

The bytemove command copies 3 bytes from memory location psampsBuf to memory location ppackBuf (a longmove command would have moved 3 longs).

The expression long[pcomprCodeBuf] := CODE24 says to treat the address pcomprCodeBuf as a long and to write to all 4 bytes following that address.

A function/method definition in Spin looks like this:

 1  VAR
 2    long globalVar
 3
 4  PUB MYFUN(arg1, arg2) : retVal | localVar
 5    ' globalVar is available for use and can be modified
 6    ' globalVar changes will be seen outside the function
 7    '
 8    ' use arg1 and arg2 inside the function
 9    '
10    ' localVar is available here and is local to the
11    ' function. It isn't available outside the function.
12    '
13    ' set the return value retVal
14  return ' this will return retVal to the calling program

The arguments arg1 and arg2, the return value retVal, and the local variable localVar are all optional .

5.5 Testing

Here are the tests that I run. As I was developing the code, I started out with the simpler tests (testing that sample 0 was packed correctly and then sample 1) and moved to successively more complex tests (sample 15, and so on).

TEST_THAT_SAMP0_IS_PROPERLY_PACKED
TEST_THAT_SAMP0_SETS_NCOMPR_CORRECTLY
TEST_THAT_SAMP0_SETS_COMPRCODE_CORRECTLY
TEST_THAT_COMPRESSOR_FAILS_FOR_NSAMPS_WRONG
TEST_THAT_SAMP1_IS_PROPERLY_PACKED_ONE_BYTE
TEST_THAT_SAMP1_IS_PROPERLY_PACKED_TWO_BYTES
TEST_THAT_SAMP1_IS_PROPERLY_PACKED_THREE_BYTES
TEST_THAT_SAMP1_SETS_COMPRCODE_CORRECTLY
TEST_THAT_SAMP1_SETS_COMPRCODE_CORRECTLY_TWO_BYTES
TEST_THAT_SAMP15_PACKS_PROPERLYTEST_THAT_SAMP16_PACKS_PROPERLY
TEST_THAT_SAMP127_PACKS_PROPERLY
TEST_THAT_SAMP0_IS_PROPERLY_UNPACKED
TEST_THAT_SAMP1_IS_PROPERLY_UNPACKED
TEST_THAT_128_SAMPS_PROPERLY_COMPRESS_AND_DECOMPRESS

I built up the code by writing the test and then writing the code that lets the test succeed. As I built up the code bit by bit, I made sure all the tests were run every time I modified the program and that they all passed.

$ propeller-load -t -r -p /dev/cu.usbserial-A103FFE0 steim_spin_Demo.binary
Propeller Version 1 on /dev/cu.usbserial-A103FFE0
Loading steim_spin_Demo.binary to hub memory
7568 bytes sent
Verifying RAM ... OK
[Entering terminal mode. Type ESC or Control-C to exit.]
Compression
mainCogId:     0
Test that sample 0 is properly packed to packBuf
...ok
<<<...output deleted...>>>
Test that compression and decompression of 128 random numbers is successful
...ok
Tests Run: 20
Tests Passed: 20
Tests Failed: 0

5.6 Final Code

I won’t go through the compression and decompression routines in detail because the focus of this book is on PASM (and they are quite self-explanatory). However, I reproduce them here because I’ll refer to them when I am developing the PASM code.

5.6.1 Compression in Spin

The full compression code first checks that the inputs are correct. Next, the first sample is compressed, and then the remaining samples are differenced and compressed. The driver file (steim_spin1_Demo.spin) will call the code in Listings 5-3 and 5-4 (which are in file steim_spin1.spin).

 1  PUB COMPRESS(psampsBuf, ns, ppackBuf, pcomprCodesBuf) : ncompr | j, diff, adiff, codeIdx, codeShift
 2  " Inputs :
 3  "   psampsBuf - address of sampsBuf array (long array).
 4  "   ns - length of `sampsBuf ` (number of samps to compress).
 5  "   ppackBuf - address of `packBuf ` array (byte array) where
 6  "              samples are to be packed.
 7  " pcomprCodesBuf - address of array of compresion codes
 8  "                  (long array) - 16 codes per long.
 9  " Output :
10  "   ncompr - length of packed array (bytes)
11      if ns == 0 ' this isn't an error - but do nothing
12          return 0
13
14      if (ns < 0) | (ns > _nsmax)
15          return -1
16
17      ' handle sample0 first - it is always packed to 3 bytes
18      ' regardless of its size
19      bytemove(ppackBuf, psampsBuf, 3)
20      ncompr := 3
21      long[pcomprCodesBuf] := CODE24
22
23      if ns == 1
24          return ncompr
25
26      repeat j from 1 to ns -1
27          diff := long[psampsBuf][j] - long[psampsBuf][j -1]
28          adiff := || diff
29          codeIdx := j / 16
30          codeShift := (j // 16) * 2
31          if adiff < $7F
32              bytemove(ppackBuf + ncompr, @diff, 1)
33              ncompr++
34              long[pcomprCodesBuf][codeIdx] |= CODE08 << codeShift
35          elseif adiff < $7FFF
36              bytemove(ppackBuf + ncompr, @diff, 2)
37              ncompr += 2
38              long[pcomprCodesBuf][codeIdx] |= CODE16 << codeShift
39          else
40              bytemove(ppackBuf + ncompr, @diff, 3)
41              ncompr += 3
42              long[pcomprCodesBuf][codeIdx] |= CODE24 << codeShift
43
44      return ncompr
Listing 5-3

steim spin1.spin: Complete Compressor Code Listing

  • Lines 11–21: Verify inputs and compress sample 0.

  • Line 27: Form the backward difference δ j = s j − s j−1 .

  • Line 28: Calculate the absolute value a j = j |.

  • Lines 29–30: The compression codes are 2 bits each, so the codes for samples s j , j = 0–15 are in comprCodesBuf[0], the codes for samples s j , j=16–31 are in comprCodesBuf[1], and so on. The j-th code is shifted by (j mod 16) × 2 bits.

  • Lines 31-42: Depending on whether a j fits in 1, 2, or 3 bytes, place the difference in packBuf and place the code in comprCodeBuf .

5.6.2 Decompression in Spin

The decompression is a little more involved, mainly because negative numbers have to be sign-extended properly. The general outline is the same as for compression: handle the first sample and then loop through the remaining samples.

 1  PUB DECOMPRESS(psampsBuf, ns, ppackBuf, ncompr, pcomprCodesBuf) : ndecomp | diff, pkIdx, codeIdx, codeShift, theComprLong, jcomprCode, pkBytes, shneg
 2  " Inputs :
 3  "   psampsBuf - address of sampsBuf array
 4  "   ns - length of 'sampsBuf' (number of samps to decompress)
 5  "   ppackBuf - address of 'packBuf' array where samples are packed
 6  "   ncompr - length of packBuf
 7  "   pcomprCodesBuf - array of compresion codes (16 codes per long)
 8  " Output :
 9  "   ndecomp - number of samples decompressed (should be same as ns)
10
11      ' check inputs
12      if ns == 0 ' this isn't an error - but do nothing
13          return 0
14
15      if (ns < 0) | (ns > _nsmax)
16          return -1
17
18      ' init
19      ndecomp := 0
20      pkIdx := 0
21
22      repeat while ns > ndecomp
23          ' codeIdx - index into the comprCodesBuf array
24          ' codeShift - index into the actual code long where
25          '   the code for this sample is stored
26          codeIdx := ndecomp / 16
27          codeShift := ( ndecomp // 16) * 2
28
29          theComprLong := long[pcomprCodesBuf][codeIdx]
30          jcomprCode := (theComprLong >> codeShift) & %11
31          case jcomprCode
32              CODE08 :
33                  bytemove(@diff, ppackBuf + pkIdx, 1)
34                  diff <<= 24 ' sign extend
35                  diff ~>= 24
36                  pkBytes := 1
37              CODE16 :
38                  bytemove(@diff, ppackBuf + pkIdx, 2)
39                  diff <<= 16 ' sign extend
40                  diff ~>= 16
41                  pkBytes := 2
42              CODE24 :
43                  bytemove(@diff, ppackBuf + pkIdx, 3)
44                  diff <<= 8 ' sign extend
45                  diff ~>= 8
46                  pkBytes := 3
47
48          pkIdx += pkBytes
49          ncompr -= pkBytes
50
51          if ndecomp == 0 ' samp 0 is packed as is - not a difference
52              theSamp := diff
53          else
54              theSamp := long[psampsBuf][ndecomp -1] + diff
55
56          theSamp <<= 8
57          theSamp ~>= 8
58          long[psampsBuf][ndecomp] := theSamp
59          ndecomp++
60
61          if ncompr < 0 ' error check on ncompr
62              return -1
63      return ndecomp
Listing 5-4

steim_spin.spin: Spin Decompression Method

  • Lines 26–30: Determine the compression code for this sample by getting the correct long from the comprCodesBuf array and then getting the appropriate 2 bits.

  • Lines 31–46: Copy the difference bytes (either 1, 2, or 3, depending on the compression code for this sample) from packBuf and sign-extend them to form a proper 32-bit δ j . The operations diff <<= 24 and diff ~>= 24 will first shift the low byte up to the high byte location and then do an arithmetic shift back to the low byte. An arithmetic shift retains the sign of the byte. So, if the low byte was a negative 1-byte number (-127 to -1), then the 32-bit number would still be negative (with 1s in all the high bits).

  • Lines 51–58: Form the sample s j = s j−1 + δ j and sign-extend.

As we build up the PASM code , we have to reproduce each of these steps, including the loops.

5.7 The Need for Speed

The main reason to implement the code in PASM is to speed it up. Let’s see how much time the compression and decompression take in Spin. Listing 5-5 shows the timing code in the driver file steim_spin1_Demo.spin.

 1  PUB MAIN | t0, dt, j
 2  ...
 3    ' time
 4    nsamps := 128
 5    repeat j from 0 to nsamps-1
 6      sampsBuf[j] := j * 50000
 7
 8    t0 := cnt
 9    nc := STEIM.COMPRESS(@sampsBuf, nsamps, @packBuf, @comprCodeBuf)
10    dt := cnt - t0
11    UARTS.STR(DEBUG, string(" nc= "))
12    UARTS.DEC(DEBUG, nc)
13    UARTS.PUTC(DEBUG, CR)
14    UARTS.STR(DEBUG, string(" dt= "))
15    UARTS.DEC(DEBUG, dt)
16    UARTS.PUTC(DEBUG, CR)
17    UARTS.STR(DEBUG, string(" dt(ms) ~ "))
18    UARTS.DEC(DEBUG, dt / ONE_MS)
19    UARTS.PUTC(DEBUG, CR)
20  ...
Listing 5-5

Measuring the Timing of Compression

Running this code prints out the following:

...
nc= 382
dt= 1526096
dt (ms) ~ 19

Here, nc is the number of bytes in the compressed buffer packBuf, and the two dt values are the time taken to perform the compression (in clock cycles and milliseconds, respectively).

If your application is OK with taking 20ms to compress 128 samples, then you can stop right here. If not, let’s estimate how much of a speedup we can expect in PASM.

5.7.1 Timing in PASM

Each instruction in PASM takes a fixed number of clock cycles that are documented in the manual. Almost all the instructions take four clock cycles exactly.1

A clock cycle is 12.5ns (when the system clock is running at 80MHz), so typically instructions require 50ns to complete.

The system clock is determined by the constants shown in Listing 5-6 in the main Spin file.

1  CON ' Clock mode settings
2    _CLKMODE = XTAL1 + PLL16X
3    _XINFREQ = 5_000_000
Listing 5-6

Clock Mode Settings

_XINFREQ is the frequency of the external crystal. The _CLKMODE variable sets the clock to 16 times the crystal frequency (in this case, 80MHz).

5.7.2 PASM Timing Estimate

During the compression, there is some setup, and then all the samples are processed.

For each of the 128 samples, something like the following must happen:
  • Read a sample from the hub (20 clock cycles).

  • Subtract from the previous one (5 instructions, 20 cycles).

  • Take the absolute value (5 instructions, 20 cycles).

  • Check for the length of the absolute value (10 instructions, 40 cycles).

  • Calculate the compression code location (5 instructions, ∼20 cycles).

  • Write up to 3 bytes to the hub (60 cycles).

The initialization and finalization may look something like this:
  • Initialize the variables (10 instructions, 40 cycles).

  • Take care of the overhead, in other words, writing the compression code to the hub every 16 samples, or 8 times for 128 samples (20 cycles × 8 160 cycles).

The total is approximately 25,000 clock cycles, or 0.3ms (with an 80MHz clock). This is a speedup factor of 60, so if the data rates in your application are high, it will be worth going to the trouble of implementing it in PASM (in Figure 5-2, I show an example of things that go at different speeds!).
../images/459910_1_En_5_Chapter/459910_1_En_5_Fig2_HTML.jpg
Figure 5-2

Bullock cart in front of Victoria Terminus (“The most magnificent railway station in the world,” Bombay, India, 1903. https://upload.wikimedia.org/wikipedia/commons/f/f4/Victoriaterminus1903.JPG .

5.8 Summary

In this chapter, we worked through a full example of using Test-Driven Development to build and verify a Steim compressor and decompressor . The code is pure Spin, with a driver file (with a Demo suffix) and a worker file with functions (also known as methods) that are called from the driver. The tests are in the driver file, and each one exercises one small part of the specification.

I showed how a function/method is defined and called, how to test the inputs for reasonableness, how to copy bytes out of a long, and how to correctly copy them back in during decompression. I discussed how to pass variables and arrays to a Spin function and how to get a return value.

Finally, you learned how measure the time taken to process an array.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.158.47