mirror of
https://git.k-space.ee/arti/vt1200.git
synced 2026-10-31 12:05:05 +02:00
udpstream: LANCE command payload -- peek/poke/jump/reboot, benchmark, driver-matched RX
lance_cmd.c is a payload that drives the LANCE with its own code (no ROM
calls) and answers a command protocol on ethertype 0x88B6, so a unit whose
chip we have taken over can still be inspected:
read (peek), stats, poke, jump, reboot
The reply path was dead for a while and the cause was not the chip: the RX
ring had been grown to 16 entries (0x3020..0x309F) while TXR stayed at
0x3060, so all eight TX descriptors lay inside the RX ring. The chip's RX
write-back set OWN in what we believed were TX descriptors, and every reply
was discarded by transmit_poll() as "owned but not start of packet"
(r 00309A 8000 / w 00309A 0000 in the DMA log). TX ring moved to 0x30C0,
with a compile-time guard so the rings cannot overlap again.
Receive path aligned with the three drivers available locally (4.4BSD
pmax if_le.c, NetBSD am7990.c, Linux 7990.c):
* STP *and* ENP required on one descriptor, else the frame is counted as
chained and dropped rather than parsed from the middle of a buffer;
* the 4-byte FCS the chip writes into the buffer is subtracted, as all
three do (rmd3 - 4);
* re-arm restores RMD2 and clears RMD3, ownership written last;
* TX kick is TDMD|INEA (no reference driver re-writes STRT);
* a TX slot still owned is skipped instead of clobbered.
RX buffers are 1526 B so a full 1514-byte MTU frame fits one descriptor;
1500 B would have chained it.
vtcmd.py drives all of it; bench-sweep.sh sweeps size x rate. Measured on
the real VT1200 (10 Mbit half-duplex hub): 856-858 fps of 1514-byte frames
at 99.9-100% delivery, i.e. wire limited, and 1500 fps of 60-byte frames at
99.8-99.9%. NETWORK_REPORT.md sections 68-72 record the evidence, including
the 64-descriptor build that died with an illegal opcode on the unit and was
reverted.
This commit is contained in:
1 parent
e51c7b94cf
commit
0563cb08e8
5 files changed
+1066
No files matched your search
@@ -4479,3 +4479,219 @@ does so with no ROM code: our init block, our rings, our descriptors, our
|
||||
|
||||
Both are fixed: `X` added, and the MAC is now one word per row (`MAC0`/`MAC1`/
|
||||
`MAC2`) with `REST` moved to y 878.
|
||||
|
||||
## 68. Ring overlap: the reply was discarded by the chip, not by the wire
|
||||
|
||||
`lance_cmd.c` (the command/benchmark payload) received commands correctly —
|
||||
`cmds` incremented and `laststat` was `0` — but no reply ever reached the host.
|
||||
The TX descriptor read back as `TMD1 = 0x8000` (OWN set, STP and ENP clear)
|
||||
although the code had written `0x8300`.
|
||||
|
||||
The DMA log (`VT1200_LANCE_DMA_LOG`) showed the chip doing this on **all eight**
|
||||
TX descriptors, round-robin, forever:
|
||||
|
||||
```
|
||||
r 00309A 8000 FFFF pc=018C9B00 cyc=275084712
|
||||
w 00309A 0000 FFFF pc=018C9B00 cyc=275084712
|
||||
```
|
||||
|
||||
Read `0x8000`, write back `0x0000` — that is `transmit_poll()`'s "owned but not
|
||||
start of packet" path (`am79c90.cpp:329`), which clears ownership and advances
|
||||
the ring without transmitting anything.
|
||||
|
||||
The cause was an address overlap, and it was arithmetic, not the chip:
|
||||
|
||||
```
|
||||
RX ring: 0x3020 .. 0x309f (16 entries x 8 B)
|
||||
TX ring: 0x3060 .. 0x309f
|
||||
```
|
||||
|
||||
The RX ring had been grown from 8 entries to 16 (to survive bursts) but `TXR`
|
||||
was left at `0x3060`, so **all eight TX descriptors lay inside the RX ring**.
|
||||
The chip's RX write-back set `OWN` in what the payload believed were TX
|
||||
descriptors, so every reply was rejected as a continuation fragment.
|
||||
|
||||
Fixed by moving the TX ring to `0x30C0`, plus a compile-time guard so the same
|
||||
mistake cannot compile again:
|
||||
|
||||
```c
|
||||
typedef char rx_tx_rings_do_not_overlap[(RXR + NRX * 64 <= TXR) ? 1 : -1];
|
||||
```
|
||||
|
||||
This also explains the earlier "reply buffer has the broadcast address as its
|
||||
source MAC": that was a second, independent bug (the reply copied the request's
|
||||
destination MAC into its own source field), fixed separately. A frame whose
|
||||
source is multicast is dropped by the tap, so it would have failed even with
|
||||
correct descriptors.
|
||||
|
||||
## 69. Packet handling cross-checked against the four drivers
|
||||
|
||||
Four implementations are available locally and were read line by line:
|
||||
`original-bsd/sys/pmax/dev/if_le.c` (the firmware's ancestor, section 52),
|
||||
`NetBSD-src/sys/dev/ic/am7990.c`, `linux/drivers/net/ethernet/amd/7990.c`, and
|
||||
`linux/drivers/net/ethernet/amd/7990.h`.
|
||||
|
||||
| | 4.4BSD `if_le.c` | NetBSD `am7990.c` | Linux `7990.c` | this payload |
|
||||
|---|---|---|---|---|
|
||||
| RX buffer | `LEMTU` **1518** (`if_lereg.h:13`) | `LEBLEN` **1536** (`lancereg.h:116`) | `PKT_BUFF_SIZE` **1544** (`7990.h:45`) | 1526 |
|
||||
| ring | `LERBUF` | `sc_nrbuf` | 8 RX / 2 TX (`7990.h:35-36`) | 16 RX / 8 TX |
|
||||
| RX length word | `-LEMTU` (`:344`) | `-LEBLEN \| 0xf000` (`:186`) | `-RX_BUFF_SIZE \| 0xf000` (`:200`) | `0xFA0A` |
|
||||
| complete frame | `(rmd1 & (STP\|ENP)) == (STP\|ENP)` (`:630`) | same, `"dropping chained buffer"` (`:247`) | `(bits & POK) != POK` (`:303`) | now both bits |
|
||||
| FCS | `len - 14 - 4` (`:691`) | `rmd3 - 4` (`:256`) | `(mblength & 0xfff) - 4` (`:322`) | now `- 4` |
|
||||
| re-arm | clear RMD3, then OWN (`:661`) | `OWN`, `rmd2 = -LEBLEN\|ONES`, `rmd3 = 0` (`:259`) | clear mblength, then OWN (`:346`) | now all three |
|
||||
| TX descriptor | `OWN\|STP\|ENP`, `tmd2=-len`, `tmd3=0` (`:485`) | same (`:513`) | `POK\|OWN`, `(-len)\|0xf000` (`:583`) | same |
|
||||
| TX kick | `TDMD\|INEA` (`:491`) | `INEA\|TDMD` (`:524`) | `INEA\|TDMD` (`:583`) | now `INEA\|TDMD` |
|
||||
| slot still owned | `panic("lestart")` (`:471`) | `"missing buffer"`, break (`:482`) | stop queue | skip, count `txfull` |
|
||||
|
||||
### 69.1 Why the buffer must be ≥ 1518
|
||||
|
||||
All three size the RX buffer at `ETHERMTU + 14 + 4`: the chip writes the
|
||||
**FCS** into the buffer and includes it in `RMD3`. The payload's counter showed
|
||||
this before it was fixed — a 60-byte frame was counted as 64 bytes. The earlier
|
||||
1526 was therefore already legal; a 1500-byte buffer would not have been, and
|
||||
`RMD1_ENP` handling alone would have miscounted the tail of a chained frame as a
|
||||
whole frame.
|
||||
|
||||
### 69.2 What changed in the payload because of this
|
||||
|
||||
1. a frame must have **STP and ENP** on the same descriptor, or it is counted as
|
||||
`chained_dropped` and re-armed without being parsed;
|
||||
2. the byte counter subtracts the 4-byte FCS;
|
||||
3. re-arm restores RMD2 and clears RMD3, with ownership written last;
|
||||
4. the TX kick is `INEA|TDMD`, not `STRT|TDMD` — no reference driver re-writes
|
||||
`STRT`, and 4.4BSD and NetBSD both write exactly `TDMD|INEA`;
|
||||
5. a TX slot that is still owned is skipped and counted in `txfull`.
|
||||
|
||||
## 70. The command protocol: read and stats over the wire
|
||||
|
||||
The payload (ethertype `0x88B6`, magic `'K' 'S'`) answers two commands, which is
|
||||
what makes the machine debuggable while the OS is down:
|
||||
|
||||
| cmd | request | reply |
|
||||
|---|---|---|
|
||||
| 1 `read` | 32-bit big-endian **bit** address, 16-bit length (≤ 64) | status, then the bytes |
|
||||
| 2 `stats` | — | `RXF`, `bytes`, `cmds`, `chained_dropped`, `txfull`, `csr0_err` |
|
||||
| other | — | status `0xE002` |
|
||||
|
||||
`harness/udpstream/vtcmd.py` drives it: `stats`, `read --addr --len`, and
|
||||
`bench`. The first frame after takeover is routinely lost (the ring is armed
|
||||
before the receive loop runs), so requests are retried three times.
|
||||
|
||||
Confirmed in MAME and on the unit: `read 0x018D0000 --len 16` returned
|
||||
`01 00 00 00 55 55 …`, the payload's own MARK block — arbitrary memory over
|
||||
Ethernet, with no ROM code and no OS.
|
||||
|
||||
## 71. Receive throughput on the real VT1200
|
||||
|
||||
The unit and the host are joined by a **10 Mbit half-duplex hub** (the VT1200 has
|
||||
coax Ethernet). That fact governs every number below: above the receiver's
|
||||
capacity the *delivered* rate does not plateau, it collapses, which is what a
|
||||
half-duplex collision domain does under overload.
|
||||
|
||||
`bench` sends 60-byte and 1514-byte frames at a paced or unpaced rate, then reads
|
||||
the payload's counters. Background traffic is subtracted by taking a `before`
|
||||
reading; the payload's own control frames are subtracted explicitly
|
||||
(`frames = ΔRXF − Δcmds`).
|
||||
|
||||
| frame | offered | delivered | rate | `csr0_err` |
|
||||
|---|---|---|---|---|
|
||||
| 1514 B | 1717–1723 fps, unpaced | **99.9–100 %** | **856–858 fps, 10.37–10.39 Mbit/s** | `0x9000` |
|
||||
| 60 B | 3001 frames @ 1500 fps | **99.8–99.9 %** (5 runs) | 1498–1499 fps, 88 KiB/s | `0x9000` |
|
||||
| 60 B | 6001 frames @ 2000 fps | 58 % | 1165 fps | `0x9000` |
|
||||
| 60 B | 12001 frames @ 4000 fps | 0.3 % | 12 fps | `0x9000` |
|
||||
|
||||
Readings and their limits:
|
||||
|
||||
* **Full-MTU reception is wire-limited, not code-limited.** 856–858 fps of
|
||||
1514-byte frames is 10.37–10.39 Mbit/s of frame bytes; with preamble, FCS and
|
||||
inter-frame gap that is at or slightly above the nominal 10 Mbit rate
|
||||
(theoretical maximum for this frame size is ~813 fps at exactly 10.000 Mbit/s,
|
||||
so the link clock is a few percent fast or the sender's pacing is). Nothing was
|
||||
dropped.
|
||||
* **Minimum-size frames sustain ~1500 fps with ~100 % delivery.** Above that the
|
||||
ring overflows and the hub also collides, and the two are not separable from
|
||||
the outside.
|
||||
* `csr0_err = 0x9000` is `ERR|MISS`: **the chip did run out of descriptors**, but
|
||||
the bit is **sticky** in this build (it ORs CSR0 on every pass), so it records
|
||||
"at some point", not "how many". It was clear before the 2000/4000 fps runs and
|
||||
set afterwards, which is consistent with those runs overrunning a 16-deep ring.
|
||||
A per-event counter is the next refinement; the sticky bit cannot attribute
|
||||
the misses to a particular run.
|
||||
* The payload's `stats`-command handling stalls the receive loop for the length
|
||||
of one reply transmit, so a burst that coincides with a command can produce
|
||||
misses that a pure data burst would not.
|
||||
|
||||
### 71.1 What is proven and what is inferred
|
||||
|
||||
**Proven:** the payload receives every frame offered up to the wire limit at full
|
||||
MTU; the chip reports no error bits at that rate; 1500 fps of 60-byte frames is
|
||||
reproducible at 99.8–99.9 % over five runs; the reply path works (`stats` and
|
||||
`read` both answer on the unit).
|
||||
|
||||
**Inferred, not proven:** that the collapse above ~2000 fps of 60-byte frames is
|
||||
the half-duplex hub rather than the LANCE. `ERR|MISS` proves the ring overflowed
|
||||
at 2000–4000 fps, which is sufficient to explain *loss*; it does not explain why
|
||||
the *delivered* rate falls from 1165 fps to 12 fps as the offered rate rises —
|
||||
that shape (rather than a plateau) is characteristic of collision collapse, but
|
||||
the unit's side of the hub is not instrumented.
|
||||
|
||||
### 71.2 A 64-deep ring crashed the unit
|
||||
|
||||
Raising `NRX` to 64 (ring at `0x3020`–`0x321F`, TX ring and buffer moved to
|
||||
`0x3240`/`0x3300`) ran correctly in MAME but died on the unit with an **illegal
|
||||
opcode** during the first 60-byte burst. The 64 buffers grow the RX buffer pool
|
||||
from 24 KB (`0x4000`–`0x9F78`) to 97 KB (`0x4000`–`0x1B78A`) of low RAM, and the
|
||||
firmware's OS is still resident while the payload holds the chip. The payload was
|
||||
reverted to the proven 16-deep layout; the illegal opcode is **not reproduced in
|
||||
MAME**, so the memory hypothesis is an inference, not a demonstrated cause.
|
||||
|
||||
## 72. peek / poke / jump / reboot, so a taken-over unit can be driven
|
||||
|
||||
The command byte selects the operation; addresses are **bit** addresses
|
||||
throughout, exactly like every pointer the TMS34010 uses.
|
||||
|
||||
| cmd | name | request | reply |
|
||||
|---|---|---|---|
|
||||
| 1 | `read` / peek | `[17..20]` address, `[21..22]` length (≤ 64) | status, then the bytes |
|
||||
| 2 | `stats` | — | `RXF`, `bytes`, `cmds`, `chained_dropped`, `txfull`, `csr0_err` |
|
||||
| 3 | `poke` | `[17..20]` address, `[21..22]` length (≤ 1024), `[23..]` bytes | status, 2-byte count written |
|
||||
| 4 | `jump` | `[17..20]` address | status, 4-byte target, **then control is transferred** |
|
||||
| 5 | `reboot` | — | status, 4-byte target, **then control is transferred** |
|
||||
| — | — | — | `0xE002` unknown, `0xE001` read too long, `0xE003` poke truncated |
|
||||
|
||||
The reply is transmitted and its descriptor completion waited on **before** the
|
||||
transfer, so the host always gets an acknowledgement.
|
||||
|
||||
### 72.1 `jump` needs the entry point, not `.text`
|
||||
|
||||
The COFF entry is **`0x018CBEB0`**; `0x018C0000` is only the start of `.text`.
|
||||
Measured in MAME:
|
||||
|
||||
* `jump 0x018C0000` → payload stops answering and does not come back;
|
||||
* `jump 0x018CBEB0` → the payload restarts its own `crt` and answers again with
|
||||
counters back to zero (`RXF=1`, i.e. just the `stats` request itself).
|
||||
|
||||
### 72.2 `reboot` jumps to the ROM reset vector
|
||||
|
||||
`reboot` reads a 32-bit word from bit `0xFFFFFFE0` and jumps there. On this ROM
|
||||
that word is `0x3F200040`, the same entry the CPU takes at power-on. The first
|
||||
instructions there are (`harness/mame/reset.dasm`):
|
||||
|
||||
```
|
||||
3F200040: 0360 DINT
|
||||
3F200050: 0550 SETF 10h,0,0
|
||||
3F200060: 0740 SETF 20h,0,1
|
||||
3F200070: 5600 CLR A0
|
||||
3F200080: 0580 00D0 0640 MOVE A0,@64000D0h,0
|
||||
...
|
||||
```
|
||||
|
||||
`DINT` first, then the DUART control registers — so the entry does not depend on
|
||||
the state a payload leaves behind, and at real power-on `SP` is undefined
|
||||
anyway, which is the condition we reproduce.
|
||||
|
||||
MAME evidence: after the reply, the PC leaves the payload (`0x018Cxxxx`) for ROM
|
||||
(`0x3FB43E40` at the next sample) and the payload stops answering. What is **not**
|
||||
shown yet is that the unit comes back *without a power cycle* — in MAME the OS
|
||||
was never down, so a warm restart is indistinguishable from the OS continuing.
|
||||
That test needs the real machine and is the reason the command exists.
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/bin/sh
|
||||
# bench-sweep.sh -- measure the receive path of the LANCE command payload
|
||||
# (harness/udpstream/lance_cmd.c) across frame sizes and offered rates.
|
||||
#
|
||||
# ./bench-sweep.sh vt1200 # MAME (tap interface)
|
||||
# sudo ./bench-sweep.sh enp197s0f3u1 08:00:2b:16:4b:5a # real unit
|
||||
#
|
||||
# Output is one CSV line per run:
|
||||
#
|
||||
# size,rate,seconds,sent,frames,bytes,fps,KiB/s,Mbit/s,delivery%,csr0_err,txfull,chained
|
||||
#
|
||||
# "delivery" below 100% means frames were offered and not received. csr0_err
|
||||
# tells you which side lost them: MISS (0x1000) = the chip had no descriptor
|
||||
# (ring too small or the loop too slow); no MISS = they never reached the chip
|
||||
# (wire, switch, or the sender).
|
||||
#
|
||||
# The unpaced runs are deliberately last and slowest-to-recover: a 60-byte
|
||||
# flood is far above both the wire and the loop, and it exists only to show
|
||||
# where the ceiling is.
|
||||
set -e
|
||||
|
||||
IFACE=${1:-vt1200}
|
||||
DST=${2:-ff:ff:ff:ff:ff:ff}
|
||||
SECS=${SECS:-2}
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
|
||||
echo "size,rate,seconds,sent,frames,bytes,fps,KiB/s,Mbit/s,delivery%,csr0_err,txfull,chained"
|
||||
|
||||
for size in 60 200 500 1000 1514; do
|
||||
for rate in 500 1000 2000 4000; do
|
||||
python3 "$HERE/vtcmd.py" bench --iface "$IFACE" --dst "$DST" \
|
||||
--size "$size" --rate "$rate" --seconds "$SECS" --terse || break
|
||||
done
|
||||
# as fast as the host can send
|
||||
python3 "$HERE/vtcmd.py" bench --iface "$IFACE" --dst "$DST" \
|
||||
--size "$size" --seconds "$SECS" --terse || break
|
||||
done
|
||||
Binary file not shown.
@@ -0,0 +1,520 @@
|
||||
/*
|
||||
* lance.c -- LANCE loopback probe using LOW-MEMORY rings.
|
||||
*
|
||||
* The OS's own init block (pointer at bit 0x23E7A0 -> byte 0xF80 on a live
|
||||
* boot) puts its rings at byte 0x0A80 / 0x0E80 and stores only the LENGTH in
|
||||
* the ring words' bits 13-15 (0xE000 / 0xA000), with no address bits above 16.
|
||||
* MAME's model packs address bits 16+ into that word; if the real C-LANCE does
|
||||
* not, a high ring address like 0x380028 is seen as 0x0028. This payload
|
||||
* therefore places the block, rings and buffers below 64 KB.
|
||||
*
|
||||
* Memory used (BYTE addresses, well below 64 KB):
|
||||
* 0x3000 init block (24 B) 0x3020 RX ring (8 * 8 B) 0x3060 TX ring (8 B)
|
||||
* 0x3100 TX frame (24 B) 0x3200 RX buffer (2048 B)
|
||||
*
|
||||
* Result cells at 0x018C8000 (32-bit): 0x160/0x180 CSR0 samples,
|
||||
* 0x200 RX RMD1, 0x220 RX MCNT, 0x240 TX TMD1, 0x1A0 payload-found flag.
|
||||
*
|
||||
* Panel rows (real unit, 256-byte stride): y 670/700 = CSR0, 730 = TMD1,
|
||||
* 760 = RMD1, 790 = MCNT; blocks y 850: left = payload returned, right = TX done.
|
||||
*/
|
||||
#define RAP 0x05900000
|
||||
#define RDP 0x05800000
|
||||
#define MARK 0x018D0000 /* after .text (which now reaches 0x018CACD0) */
|
||||
#define VRAM_BASE 0x04000000
|
||||
|
||||
#define MODE_LOOP 0x0004
|
||||
#define MODE_INTL 0x0040
|
||||
#define CSR0_STOP 0x0004
|
||||
#define CSR0_STRT 0x0002
|
||||
#define CSR0_INIT 0x0001
|
||||
#define CSR0_TDMD 0x0008
|
||||
#define CSR0_IDON 0x0100
|
||||
#define RMD1_OWN 0x8000
|
||||
#define RMD1_ENP 0x0100
|
||||
#define RMD1_STP 0x0200
|
||||
#define TMD1_OWN 0x8000
|
||||
#define TMD1_ENP 0x0100
|
||||
#define TMD1_STP 0x0200
|
||||
|
||||
/* Memory layout (BYTE addresses; the rings are 8-byte descriptors):
|
||||
*
|
||||
* 0x3000 init block (12 words = 24 B)
|
||||
* 0x3020 RX ring, 16 entries -> 0x3020..0x309F
|
||||
* 0x30C0 TX ring, 8 entries -> 0x30C0..0x30FF
|
||||
* 0x3180 TX buffer
|
||||
* 0x4000 RX buffers, RXBUFLEN apart
|
||||
*
|
||||
* The TX ring used to sit at 0x3060, which was fine while the RX ring had 8
|
||||
* entries (0x3020..0x305F) but silently overlapped once it grew to 16. The
|
||||
* chip then set OWN in what we believed were our TX descriptors, so every
|
||||
* reply was discarded as "owned but not start-of-packet" and never reached
|
||||
* the wire. Keep the two rings apart: NRX*8 must not reach TXR.
|
||||
*/
|
||||
#define B(byte) ((unsigned int)(byte) * 8) /* bit address of a byte */
|
||||
#define INITB B(0x3000)
|
||||
#define RXR B(0x3020)
|
||||
#define TXR B(0x30C0)
|
||||
#define RXBUF B(0x3200)
|
||||
#define TXBUF B(0x3180)
|
||||
#define NRX 16
|
||||
#define RXBUFLEN 1526 /* one full 1514-byte MTU frame + margin */
|
||||
#define ETHCMD 0x88B6
|
||||
|
||||
/* Compile-time guard: the RX ring must end before the TX ring starts.
|
||||
* (Bit addresses: NRX descriptors of 64 bits each.) Violating this is what
|
||||
* broke the first version of this payload, and it fails silently at runtime. */
|
||||
typedef char rx_tx_rings_do_not_overlap[(RXR + NRX * 64 <= TXR) ? 1 : -1];
|
||||
|
||||
/* Re-arm one RX descriptor the way NetBSD's am7990_rint() does: message count
|
||||
* and length first, ownership last, so the chip cannot take the descriptor
|
||||
* while its length field is stale. */
|
||||
#define REARM_RX(i, buf) do { \
|
||||
w16(RXR + (i) * 64 + 48, 0); \
|
||||
w16(RXR + (i) * 64 + 32, (0x10000 - RXBUFLEN) & 0xFFFF); \
|
||||
w16(RXR + (i) * 64 + 16, RMD1_OWN | (((buf) >> 16) & 0xFF)); \
|
||||
} while (0)
|
||||
|
||||
#define RING_ENTRIES 8
|
||||
|
||||
static void cell(off, v)
|
||||
unsigned int off, v;
|
||||
{
|
||||
*(unsigned int *)(MARK + off) = v;
|
||||
}
|
||||
|
||||
static void w16(bit, v)
|
||||
unsigned int bit, v;
|
||||
{
|
||||
*(unsigned short *)bit = v;
|
||||
}
|
||||
|
||||
static unsigned int r16(bit)
|
||||
unsigned int bit;
|
||||
{
|
||||
return *(unsigned short *)bit;
|
||||
}
|
||||
|
||||
static unsigned int rb(bit)
|
||||
unsigned int bit;
|
||||
{
|
||||
return *(unsigned char *)bit;
|
||||
}
|
||||
|
||||
static void wb(bit, v)
|
||||
unsigned int bit, v;
|
||||
{
|
||||
*(unsigned char *)bit = v;
|
||||
}
|
||||
|
||||
static void rect(x, y, w, h, v)
|
||||
unsigned int x, y, w, h, v;
|
||||
{
|
||||
unsigned int row, col;
|
||||
for (row = 0; row < h; row++) {
|
||||
for (col = 0; col < w; col = col + 8) {
|
||||
*(unsigned char *)(VRAM_BASE + (y + row) * 2048 + x + col) = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void bits16(x, y, v)
|
||||
unsigned int x, y, v;
|
||||
{
|
||||
unsigned int b;
|
||||
for (b = 0; b < 16; b++) {
|
||||
rect(x + b * 30, y, 24, 24, (v & (1 << b)) ? 0xFF : 0x00);
|
||||
}
|
||||
}
|
||||
|
||||
static void dot(n)
|
||||
unsigned int n;
|
||||
{
|
||||
rect(60, 650 + n * 44, 24, 24, 0xFF);
|
||||
}
|
||||
|
||||
static void settle()
|
||||
{
|
||||
unsigned int i;
|
||||
for (i = 0; i < 30; i++) {
|
||||
}
|
||||
}
|
||||
|
||||
static void csr_w(reg, v)
|
||||
unsigned int reg, v;
|
||||
{
|
||||
*(unsigned short *)RAP = reg;
|
||||
settle();
|
||||
*(unsigned short *)RDP = v;
|
||||
settle();
|
||||
}
|
||||
|
||||
static unsigned int csr_r(reg)
|
||||
unsigned int reg;
|
||||
{
|
||||
unsigned int v;
|
||||
*(unsigned short *)RAP = reg;
|
||||
settle();
|
||||
v = *(unsigned short *)RDP;
|
||||
settle();
|
||||
return v;
|
||||
}
|
||||
|
||||
/* ---- 5x7 text -------------------------------------------------------
|
||||
* Glyphs are built at runtime: this toolchain never runs .cinit, so a
|
||||
* statically initialised table would arrive as zeros (same reason
|
||||
* harness/c-hello builds its font in code).
|
||||
*/
|
||||
static unsigned char glyph[36][7];
|
||||
static unsigned char lab_csr0[5], lab_csr1[5], lab_tmd1[5], lab_rmd1[5];
|
||||
static unsigned char lab_mcnt[5], lab_rest[5], lab_ok[3], lab_yes[4], lab_no[3];
|
||||
static unsigned char lab_mac1[5], lab_mac2[5];
|
||||
|
||||
static unsigned int gi(ch)
|
||||
unsigned int ch;
|
||||
{
|
||||
if (ch >= '0' && ch <= '9') {
|
||||
return ch - '0';
|
||||
}
|
||||
return 10 + (ch - 'A');
|
||||
}
|
||||
|
||||
static void fset(i, r0, r1, r2, r3, r4, r5, r6)
|
||||
unsigned int i, r0, r1, r2, r3, r4, r5, r6;
|
||||
{
|
||||
glyph[i][0] = r0; glyph[i][1] = r1; glyph[i][2] = r2; glyph[i][3] = r3;
|
||||
glyph[i][4] = r4; glyph[i][5] = r5; glyph[i][6] = r6;
|
||||
}
|
||||
|
||||
static void build_font()
|
||||
{
|
||||
fset(0, 0x0E,0x11,0x13,0x15,0x19,0x11,0x0E); /* 0 */
|
||||
fset(1, 0x04,0x0C,0x04,0x04,0x04,0x04,0x0E); /* 1 */
|
||||
fset(2, 0x0E,0x11,0x01,0x02,0x04,0x08,0x1F); /* 2 */
|
||||
fset(3, 0x1F,0x02,0x04,0x02,0x01,0x11,0x0E); /* 3 */
|
||||
fset(4, 0x02,0x06,0x0A,0x12,0x1F,0x02,0x02); /* 4 */
|
||||
fset(5, 0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E); /* 5 */
|
||||
fset(6, 0x06,0x08,0x10,0x1E,0x11,0x11,0x0E); /* 6 */
|
||||
fset(7, 0x1F,0x01,0x02,0x04,0x08,0x08,0x08); /* 7 */
|
||||
fset(8, 0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E); /* 8 */
|
||||
fset(9, 0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C); /* 9 */
|
||||
fset(gi('A'), 0x0E,0x11,0x11,0x1F,0x11,0x11,0x11);
|
||||
fset(gi('B'), 0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E);
|
||||
fset(gi('C'), 0x0E,0x11,0x10,0x10,0x10,0x11,0x0E);
|
||||
fset(gi('D'), 0x1E,0x11,0x11,0x11,0x11,0x11,0x1E);
|
||||
fset(gi('E'), 0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F);
|
||||
fset(gi('F'), 0x1F,0x10,0x10,0x1E,0x10,0x10,0x10);
|
||||
fset(gi('K'), 0x11,0x12,0x14,0x18,0x14,0x12,0x11);
|
||||
fset(gi('M'), 0x11,0x1B,0x15,0x15,0x11,0x11,0x11);
|
||||
fset(gi('N'), 0x11,0x19,0x19,0x15,0x13,0x13,0x11);
|
||||
fset(gi('O'), 0x0E,0x11,0x11,0x11,0x11,0x11,0x0E);
|
||||
fset(gi('R'), 0x1E,0x11,0x11,0x1E,0x14,0x12,0x11);
|
||||
fset(gi('S'), 0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E);
|
||||
fset(gi('T'), 0x1F,0x04,0x04,0x04,0x04,0x04,0x04);
|
||||
fset(gi('X'), 0x11,0x11,0x0A,0x04,0x0A,0x11,0x11);
|
||||
fset(gi('Y'), 0x11,0x11,0x0A,0x04,0x04,0x04,0x04);
|
||||
}
|
||||
|
||||
static void put4(a, c0, c1, c2, c3)
|
||||
unsigned char *a;
|
||||
unsigned int c0, c1, c2, c3;
|
||||
{
|
||||
a[0] = c0; a[1] = c1; a[2] = c2; a[3] = c3; a[4] = 0;
|
||||
}
|
||||
|
||||
static void draw_glyph(x, y, ch)
|
||||
unsigned int x, y, ch;
|
||||
{
|
||||
unsigned int r, c, row, idx;
|
||||
|
||||
idx = gi(ch);
|
||||
for (r = 0; r < 7; r++) {
|
||||
row = glyph[idx][r];
|
||||
for (c = 0; c < 5; c++) {
|
||||
if (row & (1 << (4 - c))) {
|
||||
rect(x + c * 8, y + r * 3, 8, 3, 0xFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_text(x, y, s)
|
||||
unsigned int x, y;
|
||||
unsigned char *s;
|
||||
{
|
||||
while (*s) {
|
||||
if (*s != ' ') {
|
||||
draw_glyph(x, y, *s);
|
||||
}
|
||||
x = x + 48;
|
||||
s = s + 1;
|
||||
}
|
||||
}
|
||||
|
||||
static void hex4(x, y, v)
|
||||
unsigned int x, y, v;
|
||||
{
|
||||
unsigned int d, nib;
|
||||
|
||||
/* Wipe the field first. Only the glyph pixels were being written before,
|
||||
* so 0005 drawn over 1234 left the tails of the old digits on screen. */
|
||||
rect(x, y, 200, 24, 0x00);
|
||||
|
||||
for (d = 0; d < 4; d++) {
|
||||
nib = (v >> ((3 - d) * 4)) & 0xF;
|
||||
if (nib < 10) {
|
||||
draw_glyph(x + d * 48, y, '0' + nib);
|
||||
} else {
|
||||
draw_glyph(x + d * 48, y, 'A' + nib - 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
unsigned int i, n, idle, rxf, bytes, cmds, laststat, txslot, ncmd;
|
||||
unsigned int buf, len, et, j, rmd1, chaindrop, txfull, errbits;
|
||||
unsigned int jaddr, dojump;
|
||||
|
||||
cell(0x000, 1);
|
||||
build_font();
|
||||
put4(lab_csr0, 'R', 'X', 'F', ' ');
|
||||
put4(lab_csr1, 'K', 'B', ' ', ' ');
|
||||
put4(lab_tmd1, 'C', 'M', 'D', ' ');
|
||||
put4(lab_rmd1, 'S', 'T', 'A', 'T');
|
||||
put4(lab_mcnt, 'C', 'S', 'R', '0');
|
||||
put4(lab_rest, 'R', 'E', 'S', 'T');
|
||||
dot(0);
|
||||
|
||||
/* ---- chip: normal mode, our MAC, broadcast accepted ---- */
|
||||
csr_w(0, CSR0_STOP);
|
||||
w16(INITB + 0 * 16, 0x0000);
|
||||
w16(INITB + 1 * 16, 0x0008);
|
||||
w16(INITB + 2 * 16, 0x162b);
|
||||
w16(INITB + 3 * 16, 0x5a4b);
|
||||
w16(INITB + 4 * 16, 0); w16(INITB + 5 * 16, 0);
|
||||
w16(INITB + 6 * 16, 0); w16(INITB + 7 * 16, 0);
|
||||
w16(INITB + 8 * 16, (RXR >> 3) & 0xFFFF);
|
||||
w16(INITB + 9 * 16, 4 << 13); /* 16 RX descriptors */
|
||||
w16(INITB + 10 * 16, (TXR >> 3) & 0xFFFF);
|
||||
w16(INITB + 11 * 16, 3 << 13); /* 8 TX descriptors */
|
||||
|
||||
for (i = 0; i < NRX; i++) {
|
||||
buf = 0x4000 + i * RXBUFLEN;
|
||||
w16(RXR + i * 64 + 0, buf & 0xFFFF);
|
||||
w16(RXR + i * 64 + 16, RMD1_OWN | ((buf >> 16) & 0xFF));
|
||||
w16(RXR + i * 64 + 32, (0x10000 - RXBUFLEN) & 0xFFFF);
|
||||
w16(RXR + i * 64 + 48, 0);
|
||||
}
|
||||
for (i = 0; i < 8; i++) {
|
||||
w16(TXR + i * 64 + 0, (TXBUF >> 3) & 0xFFFF);
|
||||
w16(TXR + i * 64 + 16, ((TXBUF >> 3) >> 16) & 0xFF);
|
||||
w16(TXR + i * 64 + 32, 0xF000);
|
||||
w16(TXR + i * 64 + 48, 0);
|
||||
}
|
||||
|
||||
csr_w(1, (INITB >> 3) & 0xFFFF);
|
||||
csr_w(2, ((INITB >> 3) >> 16) & 0xFF);
|
||||
csr_w(3, 0);
|
||||
csr_w(0, 0x7F41);
|
||||
for (idle = 0; idle < 20000; idle++) {
|
||||
if (csr_r(0) & CSR0_IDON) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
csr_w(0, 0x0042);
|
||||
dot(1);
|
||||
|
||||
/* the ring is live from here: draw the panel only now, so frames
|
||||
* that arrive during the (slow) VRAM fill are not missed */
|
||||
rect(100, 650, 512, 256, 0x00);
|
||||
rect(100, 650, 512, 4, 0xFF);
|
||||
rect(100, 902, 512, 4, 0xFF);
|
||||
rect(100, 650, 8, 256, 0xFF);
|
||||
rect(608, 650, 8, 256, 0xFF);
|
||||
|
||||
|
||||
|
||||
rxf = 0; bytes = 0; cmds = 0; laststat = 0xEEEE; txslot = 0; ncmd = 0;
|
||||
chaindrop = 0; txfull = 0; errbits = 0; jaddr = 0; dojump = 0;
|
||||
|
||||
/* ---- main loop: receive, count, and serve commands ---- */
|
||||
for (idle = 0; ; idle++) {
|
||||
for (i = 0; i < NRX; i++) {
|
||||
rmd1 = r16(RXR + i * 64 + 16);
|
||||
if (rmd1 & RMD1_OWN) {
|
||||
continue;
|
||||
}
|
||||
buf = 0x4000 + i * RXBUFLEN;
|
||||
len = r16(RXR + i * 64 + 48) & 0x0FFF;
|
||||
/* Every reference driver (4.4BSD pmax if_le.c, NetBSD am7990.c,
|
||||
* Linux 7990.c) requires STP *and* ENP on the same descriptor and
|
||||
* drops a frame that spans descriptors instead of reassembling it.
|
||||
* Testing ENP alone would count the tail of a jumbo frame as a
|
||||
* frame and parse it from the middle of the buffer. */
|
||||
if ((rmd1 & (RMD1_STP | RMD1_ENP)) != (RMD1_STP | RMD1_ENP)) {
|
||||
chaindrop = chaindrop + 1;
|
||||
REARM_RX(i, buf);
|
||||
continue;
|
||||
}
|
||||
if (len < 60) {
|
||||
len = 60;
|
||||
}
|
||||
rxf = rxf + 1;
|
||||
/* the chip writes the 4-byte FCS into the buffer and includes it
|
||||
* in RMD3; all three drivers subtract it (`mblength - 4`) */
|
||||
if (len > 4) {
|
||||
bytes = bytes + len - 4;
|
||||
}
|
||||
et = (rb((buf + 12) * 8) << 8) | rb((buf + 13) * 8);
|
||||
ncmd = ncmd + 1;
|
||||
|
||||
/* command frame? ethertype 0x88B6, then 'K' 'S' <cmd> ... */
|
||||
if (et == ETHCMD && rb((buf + 14) * 8) == 'K' && rb((buf + 15) * 8) == 'S') {
|
||||
unsigned int cmd, addr, rlen, k, v;
|
||||
cmd = rb((buf + 16) * 8);
|
||||
addr = ((rb((buf + 17) * 8) << 24) | (rb((buf + 18) * 8) << 16) |
|
||||
(rb((buf + 19) * 8) << 8) | rb((buf + 20) * 8));
|
||||
rlen = (rb((buf + 21) * 8) << 8) | rb((buf + 22) * 8);
|
||||
cmds = cmds + 1;
|
||||
laststat = 0;
|
||||
|
||||
/* reply layout: [16]=cmd, [17..18]=status, [19..]=data */
|
||||
for (j = 0; j < 6; j++) {
|
||||
wb(TXBUF + j * 8, rb((buf + 6 + j) * 8)); /* dst = requester */
|
||||
}
|
||||
/* src = our own MAC. This used to copy the request's
|
||||
* destination (a broadcast address) and a frame with a
|
||||
* multicast source is dropped by the tap, so replies never
|
||||
* arrived even though the command was handled. */
|
||||
wb(TXBUF + 6 * 8, 0x08); wb(TXBUF + 7 * 8, 0x00);
|
||||
wb(TXBUF + 8 * 8, 0x2B); wb(TXBUF + 9 * 8, 0x16);
|
||||
wb(TXBUF + 10 * 8, 0x4B); wb(TXBUF + 11 * 8, 0x5A);
|
||||
wb(TXBUF + 12 * 8, 0x88); wb(TXBUF + 13 * 8, 0xB6);
|
||||
wb(TXBUF + 14 * 8, 'K'); wb(TXBUF + 15 * 8, 'S');
|
||||
wb(TXBUF + 16 * 8, cmd);
|
||||
|
||||
len = 60;
|
||||
if (cmd == 1) {
|
||||
/* read memory: addr is a bit address, rlen bytes */
|
||||
if (rlen > 64) {
|
||||
rlen = 64;
|
||||
laststat = 0xE001;
|
||||
}
|
||||
for (k = 0; k < 19 + rlen; k++) {
|
||||
wb(TXBUF + (19 + k) * 8, 0);
|
||||
}
|
||||
for (k = 0; k < rlen; k++) {
|
||||
v = rb(addr + k * 8);
|
||||
wb(TXBUF + (19 + k) * 8, v);
|
||||
}
|
||||
if (19 + rlen > 60) {
|
||||
len = 19 + rlen;
|
||||
}
|
||||
} else if (cmd == 2) {
|
||||
/* statistics: rxf(4) bytes(4) cmds(2) chaindrop(2) txfull(2) */
|
||||
wb(TXBUF + 19 * 8, (rxf >> 24) & 0xFF);
|
||||
wb(TXBUF + 20 * 8, (rxf >> 16) & 0xFF);
|
||||
wb(TXBUF + 21 * 8, (rxf >> 8) & 0xFF);
|
||||
wb(TXBUF + 22 * 8, rxf & 0xFF);
|
||||
wb(TXBUF + 23 * 8, (bytes >> 24) & 0xFF);
|
||||
wb(TXBUF + 24 * 8, (bytes >> 16) & 0xFF);
|
||||
wb(TXBUF + 25 * 8, (bytes >> 8) & 0xFF);
|
||||
wb(TXBUF + 26 * 8, bytes & 0xFF);
|
||||
wb(TXBUF + 27 * 8, (cmds >> 8) & 0xFF);
|
||||
wb(TXBUF + 28 * 8, cmds & 0xFF);
|
||||
wb(TXBUF + 29 * 8, (chaindrop >> 8) & 0xFF);
|
||||
wb(TXBUF + 30 * 8, chaindrop & 0xFF);
|
||||
wb(TXBUF + 31 * 8, (txfull >> 8) & 0xFF);
|
||||
wb(TXBUF + 32 * 8, txfull & 0xFF);
|
||||
wb(TXBUF + 33 * 8, (errbits >> 8) & 0xFF);
|
||||
wb(TXBUF + 34 * 8, errbits & 0xFF);
|
||||
} else if (cmd == 3) {
|
||||
/* poke: the request carries the bytes from [23] on.
|
||||
* `addr` is a BIT address, like every other address in
|
||||
* this payload, so a byte address must be sent as
|
||||
* byte_addr * 8. */
|
||||
unsigned int wlen;
|
||||
wlen = rlen;
|
||||
if (wlen > 1024) { /* one frame holds this */
|
||||
wlen = 1024;
|
||||
laststat = 0xE003;
|
||||
}
|
||||
for (k = 0; k < wlen; k++) {
|
||||
wb(addr + k * 8, rb((buf + 23 + k) * 8));
|
||||
}
|
||||
wb(TXBUF + 19 * 8, (wlen >> 8) & 0xFF);
|
||||
wb(TXBUF + 20 * 8, wlen & 0xFF);
|
||||
} else if (cmd == 4 || cmd == 5) {
|
||||
/* jump (4) to an address, or reboot (5): jump to the ROM
|
||||
* reset vector, which is the same entry the CPU takes at
|
||||
* power-on. Its first instruction is DINT and it then
|
||||
* initialises both DUARTs, so it does not depend on state
|
||||
* this payload leaves behind. The reply is transmitted
|
||||
* below, before control is transferred. */
|
||||
if (cmd == 5) {
|
||||
jaddr = *(unsigned int *)0xFFFFFFE0;
|
||||
} else {
|
||||
jaddr = addr;
|
||||
}
|
||||
dojump = 1;
|
||||
wb(TXBUF + 19 * 8, (jaddr >> 24) & 0xFF);
|
||||
wb(TXBUF + 20 * 8, (jaddr >> 16) & 0xFF);
|
||||
wb(TXBUF + 21 * 8, (jaddr >> 8) & 0xFF);
|
||||
wb(TXBUF + 22 * 8, jaddr & 0xFF);
|
||||
} else {
|
||||
laststat = 0xE002;
|
||||
}
|
||||
wb(TXBUF + 17 * 8, (laststat >> 8) & 0xFF);
|
||||
wb(TXBUF + 18 * 8, laststat & 0xFF);
|
||||
|
||||
/* All the reference drivers check that the ring slot is free
|
||||
* before reusing it (Linux stops the queue, NetBSD prints
|
||||
* "missing buffer", 4.4BSD panics) -- we just skip. */
|
||||
if (r16(TXR + txslot * 64 + 16) & TMD1_OWN) {
|
||||
txfull = txfull + 1;
|
||||
} else {
|
||||
w16(TXR + txslot * 64 + 48, 0); /* TMD3 */
|
||||
w16(TXR + txslot * 64 + 32, (0x10000 - len) & 0xFFFF);
|
||||
w16(TXR + txslot * 64 + 16,
|
||||
TMD1_OWN | TMD1_STP | TMD1_ENP | (((TXBUF >> 3) >> 16) & 0xFF));
|
||||
/* 4.4BSD/NetBSD/Linux all kick with TDMD|INEA only; a
|
||||
* repeated start (STRT) is not part of any of them. */
|
||||
csr_w(0, 0x0040 | CSR0_TDMD);
|
||||
for (j = 0; j < 20000; j++) {
|
||||
if (!(r16(TXR + txslot * 64 + 16) & TMD1_OWN)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
txslot = (txslot + 1) & 7;
|
||||
|
||||
if (dojump) {
|
||||
/* the reply has been transmitted (or waited on) above;
|
||||
* from here this payload does not return */
|
||||
void (*fp)();
|
||||
fp = (void (*)()) jaddr;
|
||||
(*fp)();
|
||||
}
|
||||
}
|
||||
/* re-arm immediately (length and count first, ownership last, as
|
||||
* NetBSD does: rmd1_bits = OWN; rmd2 = -LEBLEN|ONES; rmd3 = 0) */
|
||||
REARM_RX(i, buf);
|
||||
}
|
||||
/* sticky chip error bits: ERR|BABL|CERR|MISS|MERR. MISS set means
|
||||
* the chip had no descriptor to receive into (ring too small / loop
|
||||
* too slow); clear means the frames never reached the chip. */
|
||||
errbits = errbits | (csr_r(0) & 0xF800);
|
||||
|
||||
/* publish counters as we go */
|
||||
cell(0x160, rxf); cell(0x180, bytes); cell(0x1A0, cmds); cell(0x200, laststat);
|
||||
if ((idle & 0xFFF) == 0) {
|
||||
draw_text(104, 654, lab_csr0); hex4(312, 654, rxf);
|
||||
draw_text(104, 686, lab_csr1); hex4(312, 686, bytes >> 10);
|
||||
draw_text(104, 718, lab_tmd1); hex4(312, 718, cmds);
|
||||
draw_text(104, 750, lab_rmd1); hex4(312, 750, laststat);
|
||||
draw_text(104, 782, lab_mcnt); hex4(312, 782, csr_r(0));
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
#!/usr/bin/env python3
|
||||
"""vtcmd.py -- talk to the LANCE command payload (harness/udpstream/lance_cmd.c).
|
||||
|
||||
The payload runs entirely on its own (no OS stack), so this is the only way to
|
||||
inspect the machine while it owns the chip.
|
||||
|
||||
Protocol, ethertype 0x88B6, both directions:
|
||||
|
||||
[14] 'K' [15] 'S' [16] cmd
|
||||
cmd 1 read : [17..20] address (32-bit big-endian BIT address), [21..22] len
|
||||
cmd 2 stats: no arguments
|
||||
cmd 3 poke : [17..20] address (BIT), [21..22] len, [23..] the bytes
|
||||
cmd 4 jump : [17..20] address (BIT) -- reply is sent, then control moves
|
||||
cmd 5 reboot: no arguments -- jumps to the ROM reset vector
|
||||
reply : [16] cmd echoed, [17..18] status, [19..] data
|
||||
|
||||
Every address in this protocol is a BIT address, the way the TMS34010 sees
|
||||
memory. For a byte address, send byte_addr * 8.
|
||||
|
||||
Usage:
|
||||
|
||||
# one-off statistics (frames/bytes the payload has seen)
|
||||
sudo python3 vtcmd.py stats --iface vt1200
|
||||
|
||||
# write 4 bytes at bit address 0x018D0300 (byte 0x31A60)
|
||||
sudo python3 vtcmd.py poke --iface vt1200 --addr 0x018D0300 --data "de ad be ef"
|
||||
|
||||
# run code at an address, or soft-reset the machine back into the firmware
|
||||
sudo python3 vtcmd.py jump --iface vt1200 --addr 0x018C0000
|
||||
sudo python3 vtcmd.py reboot --iface vt1200
|
||||
|
||||
# read 32 bytes of memory at bit address 0x018D0000
|
||||
sudo python3 vtcmd.py read --iface vt1200 --addr 0x018D0000 --len 32
|
||||
|
||||
# throughput: blast N frames, then ask the payload how many it counted
|
||||
sudo python3 vtcmd.py bench --iface vt1200 --count 4000 --size 1514
|
||||
"""
|
||||
import argparse
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
|
||||
ETHCMD = 0x88B6
|
||||
VT_MAC = bytes.fromhex("08002b164b5a")
|
||||
|
||||
|
||||
def parse_mac(s):
|
||||
return bytes(int(p, 16) for p in s.split(":"))
|
||||
|
||||
|
||||
def iface_mac(iface):
|
||||
with open("/sys/class/net/%s/address" % iface) as f:
|
||||
return parse_mac(f.read().strip())
|
||||
|
||||
|
||||
def open_sock(iface):
|
||||
s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(ETHCMD))
|
||||
s.bind((iface, 0))
|
||||
return s
|
||||
|
||||
|
||||
def frame(iface, cmd, addr=0, length=0, size=60, payload=b""):
|
||||
src = iface_mac(iface)
|
||||
body = bytes([ord('K'), ord('S'), cmd]) + struct.pack(">I", addr) + \
|
||||
struct.pack(">H", length) + payload
|
||||
body = body + bytes(max(0, size - 14 - len(body)))
|
||||
return b"\xff" * 6 + src + struct.pack(">H", ETHCMD) + body
|
||||
|
||||
|
||||
def request(sock, iface, cmd, addr=0, length=0, timeout=2.0, tries=3,
|
||||
payload=b""):
|
||||
"""Send a command and wait for its reply.
|
||||
|
||||
Retried because the first frame after the payload takes the chip over is
|
||||
routinely lost: the ring is armed slightly before the receive loop runs.
|
||||
"""
|
||||
for attempt in range(tries):
|
||||
sock.send(frame(iface, cmd, addr, length,
|
||||
size=max(60, 23 + len(payload)), payload=payload))
|
||||
end = time.time() + timeout
|
||||
sock.settimeout(0.2)
|
||||
while time.time() < end:
|
||||
try:
|
||||
data, addr_in = sock.recvfrom(2048)
|
||||
except socket.timeout:
|
||||
continue
|
||||
if len(data) < 19:
|
||||
continue
|
||||
if data[12:14] != struct.pack(">H", ETHCMD):
|
||||
continue
|
||||
if data[14:16] != b"KS":
|
||||
continue
|
||||
if data[16] != cmd:
|
||||
continue
|
||||
status = (data[17] << 8) | data[18]
|
||||
return status, data[19:]
|
||||
return None, None
|
||||
|
||||
|
||||
def cmd_stats(sock, iface, quiet=False):
|
||||
status, data = request(sock, iface, 2)
|
||||
if status is None:
|
||||
if not quiet:
|
||||
print("no reply -- is the payload running and the link up?")
|
||||
return None
|
||||
rxf, nbytes, cmds, chaindrop, txfull, errbits = struct.unpack(">IIHHHH", data[:16])
|
||||
flags = []
|
||||
if errbits & 0x1000:
|
||||
flags.append("MISS(no descriptor)")
|
||||
if errbits & 0x0800:
|
||||
flags.append("MERR")
|
||||
if errbits & 0x4000:
|
||||
flags.append("BABL")
|
||||
if errbits & 0x2000:
|
||||
flags.append("CERR")
|
||||
if errbits & 0x8000:
|
||||
flags.append("ERR")
|
||||
if not quiet:
|
||||
print("payload: RXF=%d bytes=%d cmds=%d chained_dropped=%d txfull=%d "
|
||||
"csr0_err=%#06x%s status=%#06x"
|
||||
% (rxf, nbytes, cmds, chaindrop, txfull, errbits,
|
||||
(" [" + ",".join(flags) + "]") if flags else " [none]", status))
|
||||
return rxf, nbytes, cmds, chaindrop, txfull, errbits
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("cmd", choices=["stats", "read", "poke", "jump",
|
||||
"reboot", "bench"])
|
||||
ap.add_argument("--iface", default="vt1200")
|
||||
ap.add_argument("--addr", type=lambda v: int(v, 0), default=0)
|
||||
ap.add_argument("--len", type=int, default=32)
|
||||
ap.add_argument("--data", default="", help="poke: bytes, e.g. 'de ad be ef'")
|
||||
ap.add_argument("--file", default="", help="poke: read the bytes from a file")
|
||||
ap.add_argument("--count", type=int, default=0,
|
||||
help="send exactly N frames instead of a timed burst")
|
||||
ap.add_argument("--seconds", type=float, default=3.0,
|
||||
help="duration of the timed burst when --count is 0")
|
||||
ap.add_argument("--size", type=int, default=1514)
|
||||
ap.add_argument("--etype", type=lambda v: int(v, 0), default=0x88B7,
|
||||
help="ethertype for benchmark frames (must NOT be 0x88B6, "
|
||||
"or the payload answers every one of them)")
|
||||
ap.add_argument("--dst", default="ff:ff:ff:ff:ff:ff",
|
||||
help="destination MAC; unicast avoids the switch's "
|
||||
"broadcast storm control")
|
||||
ap.add_argument("--rate", type=float, default=0,
|
||||
help="pace to at most N frames/s (0 = as fast as possible)")
|
||||
ap.add_argument("--busy", action="store_true",
|
||||
help="busy-wait instead of time.sleep while pacing; "
|
||||
"sleep granularity is ~250us and bunches frames "
|
||||
"into microbursts that the switch drops")
|
||||
ap.add_argument("--terse", action="store_true",
|
||||
help="one CSV line per run, for sweeps and for the report")
|
||||
ap.add_argument("--timeout", type=float, default=2.0)
|
||||
a = ap.parse_args()
|
||||
|
||||
sock = open_sock(a.iface)
|
||||
|
||||
if a.cmd == "stats":
|
||||
return 0 if cmd_stats(sock, a.iface) else 1
|
||||
|
||||
if a.cmd == "read":
|
||||
status, data = request(sock, a.iface, 1, a.addr, a.len, a.timeout)
|
||||
if status is None:
|
||||
print("no reply")
|
||||
return 1
|
||||
print("read %#010x len %d status %#06x:" % (a.addr, a.len, status))
|
||||
for i in range(0, min(len(data), a.len), 16):
|
||||
print(" %04x %s" % (i, data[i:i + 16].hex(" ")))
|
||||
return 0
|
||||
|
||||
if a.cmd == "poke":
|
||||
if a.file:
|
||||
with open(a.file, "rb") as f:
|
||||
blob = f.read()
|
||||
else:
|
||||
blob = bytes(int(b, 16) for b in a.data.replace(",", " ").split())
|
||||
if not blob:
|
||||
print("nothing to poke: use --data or --file")
|
||||
return 1
|
||||
status, data = request(sock, a.iface, 3, a.addr, len(blob), a.timeout,
|
||||
payload=blob)
|
||||
if status is None:
|
||||
print("no reply")
|
||||
return 1
|
||||
print("poked %d bytes at bit %#010x (byte %#x) status %#06x"
|
||||
% (len(blob), a.addr, a.addr >> 3, status))
|
||||
return 0
|
||||
|
||||
if a.cmd in ("jump", "reboot"):
|
||||
cmd = 4 if a.cmd == "jump" else 5
|
||||
status, data = request(sock, a.iface, cmd, a.addr, 0, a.timeout)
|
||||
if status is None:
|
||||
print("no reply")
|
||||
return 1
|
||||
target = struct.unpack(">I", data[:4])[0] if len(data) >= 4 else 0
|
||||
print("%s: control -> bit %#010x (byte %#x), status %#06x"
|
||||
% (a.cmd, target, target >> 3, status))
|
||||
print("the payload has handed control over; it will not answer again")
|
||||
return 0
|
||||
|
||||
# bench: offer frames continuously for --seconds, then let the payload's
|
||||
# counter settle. A fixed-duration burst makes the window long enough that
|
||||
# the settle time is negligible, and retrying on ENOBUFS paces the host to
|
||||
# whatever its socket buffer allows instead of aborting.
|
||||
before = cmd_stats(sock, a.iface, quiet=a.terse)
|
||||
size = max(a.size, 60)
|
||||
body = bytes(size - 14)
|
||||
f = parse_mac(a.dst) + iface_mac(a.iface) + struct.pack(">H", a.etype) + body
|
||||
sent = 0
|
||||
t0 = time.time()
|
||||
if a.count:
|
||||
for i in range(a.count):
|
||||
try:
|
||||
sock.send(f)
|
||||
sent += 1
|
||||
except OSError:
|
||||
time.sleep(0.001)
|
||||
sock.send(f)
|
||||
sent += 1
|
||||
else:
|
||||
while time.time() - t0 < a.seconds:
|
||||
if a.rate:
|
||||
want = sent / a.rate
|
||||
behind = want - (time.time() - t0)
|
||||
if behind > 0:
|
||||
if a.busy:
|
||||
end = time.time() + behind
|
||||
while time.time() < end:
|
||||
pass
|
||||
else:
|
||||
time.sleep(behind)
|
||||
try:
|
||||
sock.send(f)
|
||||
sent += 1
|
||||
except OSError:
|
||||
time.sleep(0.001)
|
||||
t1 = time.time()
|
||||
|
||||
# poll until the *data* counter is stable (each stats request is itself a
|
||||
# received command frame, so comparing rxf alone would never settle)
|
||||
after = before
|
||||
quiet = 0
|
||||
last = before[0] - before[2]
|
||||
deadline = time.time() + 15.0
|
||||
while time.time() < deadline:
|
||||
time.sleep(0.2)
|
||||
now = cmd_stats(sock, a.iface, quiet=a.terse)
|
||||
if not now:
|
||||
break
|
||||
after = now
|
||||
cur = now[0] - now[2]
|
||||
if cur == last:
|
||||
quiet += 1
|
||||
if quiet >= 2:
|
||||
break
|
||||
else:
|
||||
quiet = 0
|
||||
last = cur
|
||||
t2 = time.time()
|
||||
|
||||
if before and after and after[0] >= before[0]:
|
||||
dt = t1 - t0
|
||||
dcmds = after[2] - before[2] # our own stats frames
|
||||
frames = (after[0] - before[0]) - dcmds
|
||||
nbytes = (after[1] - before[1]) - 60 * dcmds
|
||||
if a.terse:
|
||||
print("%d,%g,%.2f,%d,%d,%d,%.0f,%.1f,%.2f,%.1f,%#06x,%d,%d"
|
||||
% (size, a.rate, dt, sent, frames, nbytes,
|
||||
frames / dt if dt else 0,
|
||||
nbytes / 1024.0 / dt if dt else 0,
|
||||
nbytes * 8 / dt / 1e6 if dt else 0,
|
||||
100.0 * frames / sent if sent else 0,
|
||||
after[5], after[4], after[3]))
|
||||
return 0
|
||||
print("sent %d frames of %d B over %.3f s (%.0f frames/s offered)"
|
||||
% (sent, size, dt, sent / dt if dt else 0))
|
||||
print("payload received %d frames (%d bytes) while they were offered"
|
||||
% (frames, nbytes))
|
||||
print(" receive rate: %.0f frames/s, %.1f KiB/s, %.1f Mbit/s"
|
||||
% (frames / dt if dt else 0,
|
||||
nbytes / 1024.0 / dt if dt else 0,
|
||||
nbytes * 8 / dt / 1e6 if dt else 0))
|
||||
print(" delivery ratio: %.1f%% (below 100%% = the ring or the wire "
|
||||
"could not keep up)" % (100.0 * frames / sent if sent else 0))
|
||||
print(" drain after the burst: %.2f s" % max(0.0, t2 - t1))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user