D0-06DR: Removing a Value From the Middle of a Table

Brian Holt8 min read
AutomationDirectHMI ProgrammingTechnical Reference
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Deleting an entry out of the middle of a V-memory table on a DL06 is two jobs: find the word, then close the gap behind it. FIND does the first job and hands back a number that looks like an address and is not one. Everything that goes wrong afterward starts there.

Read the FIND Result as an Offset, Not an Address

FIND returns the position of the match counted from the top of the table — 0 for the first word, 8 for the ninth. V-memory addresses are octal. Offset 8 into a table that starts at V2000 is V2010. V2008 does not exist, because octal has no digit 8.

The quick fixes that get tried first, and why they come back:

  • Adding the offset to the address as a decimal number. Right for offsets 0 through 7, wrong from 8 on. It passes the bench test and quietly writes into the wrong record in production.
  • Zeroing the word by hand in a Data View. Gets the shift out the door; the hole is still in the table and the operator calls again next run.
  • Reaching for the queue instructions. The add-to-top / remove-from-bottom family maintains a FIFO from the ends of a table. Neither one closes a gap in the middle.

What actually works: convert the octal table start to its binary equivalent with LDA, add the offset using binary math, store the sum in a V-word, and hand that word to the move or write instruction as a pointer (P).

Prove the arithmetic on paper before you write a rung. LDA O2000 puts 1024 decimal (0400 hex) in the accumulator. 1024 + 8 = 1032, which is octal 2010 — the ninth word of the table.

Cache the Table Start and Length in Binary

Both constants belong in V-memory once, on the first scan, so every later rung is plain register math.

ISG S0
    LDA  O144        // 100 decimal words
    OUT  V1400       // TableLength
    LDA  O2000       // table start, converted to 1024 decimal
    OUT  V1401       // TableStart
    JMP  S1

LDA reads its operand as octal. LDA O100 loads 64 decimal, not 100. If the table really is 100 words counted in decimal, the operand is O144. Settle which base your "100" was written in before anything downstream consumes it.

Register Holds Format Written by
V1400 Table length in words Same base as the address math First scan
V1401 Table start address Binary equivalent of the octal address First scan
V1402 Offset returned by FIND Position from top of table Each FIND
V1403 Words to move length − offset − 1 Before the MOV
V1404 Destination address (the hole) Binary address, used as P1404 Before the MOV
V1405 Source address (first word to shift) Binary address Before the MOV
V1410 Value to delete Same format as the table data Requesting logic

Check: single-scan into RUN and open a Data View on V1400 and V1401 in decimal. A 100-word table at V2000 reads 100 and 1024. If V1401 reads 2000, someone loaded a constant instead of using LDA.

Run the FIND and Trap the Miss in the Same Scan

SG S2
    LD    V1400      // length -> stack
    LD    V1401      // start address -> stack
    LD    K0         // begin searching at offset 0
    FIND  V1410      // value to look for lives in V1410
    OUT   V1402      // FoundOffset

    STRN  SP53
    JMP   S3         // found: go shift
    NOT
    JMP   S1         // not found: back to idle
    RST   C0
    SET   C1         // ErrorInFind -> drives Y0

The three loads fill the accumulator and stack; the offset comes back in the accumulator, which is why OUT V1402 immediately follows. SP53 comes on when the value is not in the table, and it reflects the last instruction that touched it — test it in the same scan, right after the FIND, never from a later stage.

  • FIND stops at the first match. If the same value can appear twice, delete by a unique record ID instead of by the payload.
  • Searching for K0 in a zero-filled table hits the first empty slot, not a record. Block the request when the search value is zero.
  • Confirm the maximum table length and the BCD/binary operand formats in the FIND entry of the instruction set chapter before pointing it at a long table.

Check: seed a known value in a known slot, fire the request, read V1402. The ninth word from the top returns 8.

Do the Pointer Math in Binary, Not BCD

SG S3
    LD    V1400
    SUBB  V1402
    SUBB  K1
    OUT   V1403      // NumToMove = length - offset - 1

    STR   SP1
    LD    V1401
    ADDB  V1402
    OUT   V1404      // MoveToAddress = the hole
    ADDB  K1
    OUT   V1405      // MoveFromAddress

    STR   SP1
    LD    V1403      // word count
    LD    V1405      // source address
    MOV   P1404      // destination through the pointer
    RST   C0
    JMP   S1

Why the base matters: LDA produces a binary value, and a BCD add on that pattern is right only while every hex digit stays between 0 and 9 with no carry. 0400 hex plus BCD 1 gives 0401 hex = 1025 = V2001, correct by luck. Push the offset to 10 and BCD gives 0410 hex = 1040 = V2020; the correct word is 040A hex = 1034 = V2012. The logic survives the first nine deletes and then starts overwriting live data. Use the binary math instructions and verify the constant format for them in the Math Instructions section of the DL06 user manual.

The MOV destination is a literal address unless you use the pointer form. MOV V1404 dumps the block into V1404 and onward — straight over your own working registers. Confirm that the MOV operand table accepts a pointer, then write MOV P1404. The source address needs no pointer because MOV already takes it from the accumulator.

Overlap direction is in your favor here: the destination sits one word below the source, so a low-to-high block copy leaves no smear. Do not rebuild the same shift as a descending loop.

Check: load V2000 onward with a recognizable ramp, delete the value at offset 8, and read the table. Everything below offset 8 moves up one word, and the last word still holds a duplicate.

Close the Hole at the Bottom of the Table

That trailing duplicate is what the original question was really about — writing K0 to a word whose address you only know as a number. Same pointer trick, one more scratch register:

    STR   SP1
    LD    V1401
    ADDB  V1400
    SUBB  K1
    OUT   V1406      // address of the last table word
    LD    K0
    OUT   P1406      // blank it

If zero is a legal data value in your table, do not use it as the empty marker. Keep an entry-count register instead, decrement it on every successful delete, and feed that count to FIND as the table length so a search can never match a stale word below the live data. Consumers read to the count, not to the physical end of the table.

Check: after a delete, the last word reads 0 (or the count has dropped by one), and a second FIND for the same value sets SP53.

Sequence the Delete, Then Prove It

The sequence spans several scans — idle stage, find stage, shift stage, back to idle. That is a torn table for anything reading it in between.

  1. Edge-trigger the request. C0 starts the sequence and is reset before the jump back to S1, so one request equals one delete. A level-driven bit deletes an entry on every pass.
  2. Set a busy bit when you leave the idle stage, clear it after the tail is closed, and gate every consumer of the table on it.
  3. Keep the error path visible: C1 latched by the no-hit branch, Y0 as the indicator. A bad request should light something, not fail silently.
  4. Bench the five cases before it goes near production: delete offset 0; delete the last populated entry (word count = 0 — gate the MOV on count greater than zero if a zero-length move misbehaves); delete a value that is not present; delete on an empty table; two requests back to back with the bit held on.

To get a skeleton in fast, save the RLL text as a .txt file, start a new project in DirectSOFT 5, and use File > Import > Program. Then read every rung before you download — an imported example compiles clean and still writes to the wrong words if the math base or the MOV destination is wrong.

Stop and escalate if something outside your ladder writes the same table: an option module, a Modbus master, or ASCII logic touching those words mid-shift is an arbitration problem, not a rung problem, and more ladder will not fix it. Stop as well if the pointer registers read correctly in a Data View but the move still lands outside the table or faults the CPU. Package the project file, the exact rungs, and the operand formats you used, and take it to AutomationDirect technical support rather than downloading another guess onto a running machine.

FAQ

Why does the FIND instruction return 8 instead of a V-memory address?

FIND reports the offset of the match counted from the top of the table, not an address. Convert it: LDA the octal table start (V2000 becomes 1024 decimal), add the offset in binary, store the sum, and use that word as a pointer. Offset 8 from V2000 is V2010, not V2008.

Why does my pointer math work for the first few entries and then write to the wrong word?

You are doing BCD arithmetic on a binary value from LDA. It agrees with binary math only until a hex digit exceeds 9 or a carry occurs — 0400 hex plus BCD 10 gives 1040 decimal instead of 1034. Switch the offset and length math to the binary add and subtract instructions.

Why does the deleted value still appear at the bottom of the table after the shift?

The block move copies the tail up one word and leaves the old last entry in place as a duplicate. Compute start + length − 1, write K0 through a pointer to that word, or decrement an entry-count register and have every consumer honor the count.

Why does MOV land on my working registers instead of the table?

The MOV destination operand is a literal address, so MOV V1404 writes the block into V1404 onward. Use the pointer form MOV P1404 so the block goes to the address stored in that register, and confirm the operand table for MOV accepts pointers.

Back to blog