kolja waschk
haubachstraße 39
D-22765 hamburg
+49-40-88913034
usbjtag@ixo.de
www.ixo.de

2003

I wrote the following article back in 2003/2004, but didn't publish it then (see my old posting in comp.arch.embedded about "Using ZyXEL/Netgear/... router as ARM7 development platform"). Maybe it's still useful to someone, so now (2006/2007) I finally decided to put it online anyway. I've been told that the ATEN trick also works on much newer devices...

Intention

My original intention was to try and learn developing for the ARM family of microcontroller cores, especially ARM7TDMI. I could have bought a development board for hundreds of EUR, but soon found that there are consumer products available off the shelves that provide almost the same functionality for less money.

After some searching and looking at pictures that people took from inside their devices, it appeared that a large number of DSL routers and WLAN access points are built around ARM7 or ARM9 cores. Many use chips from Samsung (e.g. S3C4510 with ARM7TDMI embedded) and Conexant (e.g. CX82100 with ARM940T; or CX84200 with ARM7TDMI, formerly made by ADMtek, called ADM5106). The Samsung chips are best documented, while information about Conexant is rare.

Another thing to look for is the availability of some method to upload and run selfmade code on the device. Often the boards in the routers provide the ARM standard JTAG connector (2x7 pin header). But from documentation available on the net, I learned that ZyXEL routers come loaded with a "BootExtension" that can be used to up- and download firmware via serial console port, and even to inspect and modify RAM contents using simple AT commands (like those AT commands used to control Hayes modems).

Finally I bought a Netgear RT311 from eBay (EUR 16!). It is almost identical to ZyXEL Prestige 310, and very similar to devices like Netgear RT314, MR314, and other ZyXEL Prestige. The most important technical specs (from my point of view) are:

This is a list of devices that probably use ZyNOS BootExtension as well; (please send corrections and updates, especially I'd like to know which ones have a serial console port or at least are prepared to have one)

The ZyNOS ("ZyXEL Network OS"?) firmware in these routers is based on Kadak AMX RTOS, the same OS that was basis for PalmOS up to version 4.x. I don't know if that's still the case for newer devices that don't use an ARM core anymore.

Accessing the router

Using the console port

Too lazy to build a JTAG cable, I tried the serial console port first. Use a normal serial cable (not a Nullmodem cable) to connect it to your PC. The initial baud rate can be configured permanently to any standard rate you want up to 115200 bps; so you might need several tries until you get a message after power-on similar to this one:

    Bootbase Version: V2.04 | 2/7/2001 18:08:22
    RAM: Size = 4096 Kbytes
    DRAM POST: Testing:  4096K OK
    FLASH: Intel 8M

    RAS Version: V3.25(M.00) | 5/4/2001 11:44:42

    Press any key to enter debug mode within 3 seconds.
    .......

Unless you hit a key, the RAS code boots and the router just works as it was meant to. But IF you hit a key, you're in debug mode. That is, you're talking to ZyXEL's "BootExtension" code. Try typing the command "ATHE" and you get this output:

    ======= Debug Command Listing =======
    AT            just answer OK
    ATHE          print help
    ATBAx         change baudrate. 1:38.4k, 2:19.2k, 3:9.6k 4:57.6k 5:115.2k
    ATENx,(y)     set BootExtension Debug Flag (y=password)
    ATSE          show the seed of password generator
    ATTI(h,m,s)   change system time to hour:min:sec or show current time
    ATDA(y,m,d)   change system date to year/month/day or show current date
    ATDS          dump RAS stack
    ATDT          dump Boot Module Common Area
    ATDUx,y       dump memory contents from address x for length y
    ATRBx         display the  8-bit value of address x
    ATRWx         display the 16-bit value of address x
    ATRLx         display the 32-bit value of address x
    ATGO(x)       run program at addr x or boot router
    ATGR          boot router
    ATGT          run Hardware Test Program
    ATRTw,x,y(,z) RAM test level w, from address x to y (z iterations)
    ATSH          dump manufacturer related data in ROM
    ATDOx,y       download from address x for length y to PC via XMODEM
    ATTD          download router configuration to PC via XMODEM
    ATUR          upload router firmware to flash ROM
    ATLC          upload router configuration file to flash ROM
    ATXSx         xmodem select: x=0: CRC mode(default); x=1: checksum mode

    OK

Now the above list does mention a lot of useful commands, but I thought there were commands to write to RAM? And what is this obscure "DebugFlag"?

At least the ATDU command allowed me to dump the whole RAM contents and then disassemble them (using arm-objdump). Locating the code that processes the ATEN and ATSE commands took me some time, but, well, learning ARM assembly language was one of my goals when I bought the device ;-)

The DebugFlag and how to change it

The ATENx[,y] command obviously wants a "password" y. If you used it without a password, it always sets the DebugFlag to zero, regardless of the value x. "Officially", the password shall be a hex value, derived from the output of the ATSE command. Now after understanding the code, it became clear that ATSE initializes an internal variable (the "seed") from the ARM's TCNT0 timer value, and outputs this value together with the last three octets of the device's (LAN) MAC address.

Further digging in the code revealed how to compute the password from the ATSE output. Only the first three octets (the "seed") and least significant 3 bits of the (LAN) MAC address are important:

    unsigned long password(unsigned long seed, unsigned char last_mac_octet)
    {
      unsigned long b = seed & 0x00FFFFFF;
      unsigned long r = ROR(b + 0x10F0A563, last_mac_octet & 7);
      return r^b;
    }

But this is still utterly complex. Just don't use ATSE, and the seed will be just zero after booting. Then, only the 3 least significant bits of the (LAN) MAC address determine the password. You can gather the MAC address from the ATSH output. It is always the same unless you change the router...

MAC Address
of LAN port
Password y for ATENx,y
...0 or ...810F0A563
...1 or ...9887852B1
...2 or ...AC43C2958
...3 or ...B621E14AC
...4 or ...C310F0A56
...5 or ...D1887852B
...6 or ...E8C43C295
...7 or ... FC621E14A

New possibilities if DebugFlag is set

On my RT311, I can now enable the DebugFlag using

    ATEN1,1887852B

And now when the DebugFlag is set, the ATHE instantly show some more possibilities (only the new ones listed here):

    ATWBx,y       write address x with  8-bit value y
    ATWWx,y       write address x with 16-bit value y
    ATWLx,y       write address x with 32-bit value y
    AT%Tx         Enable Hardware Test Program at boot up
    ATBTx         block0 write enable (1=enable, other=disable)
    ATWEa(,b,c,d) write MAC addr, Country code, EngDbgFlag, FeatureBit to flash ROM
    ATCUx         write Country code to flash ROM
    ATCB          copy from FLASH ROM to working buffer
    ATCL          clear working buffer
    ATSB          save working buffer to FLASH ROM
    ATBU          dump manufacturer related data in working buffer
    ATWMx         set MAC address in working buffer
    ATCOx         set country code in working buffer
    ATFLx         set EngDebugFlag in working buffer
    ATSTx         set ROMRAS address in working buffer
    ATSYx         set system type in working buffer
    ATVDx         set vendor name in working buffer
    ATPNx         set product name in working buffer
    ATFEx,y,...   set feature bits in working buffer
    ATMP          check & dump memMapTab
    ATUPx,y       upload to RAM address x for length y from PC via XMODEM
    ATUXx(,y)     xmodem upload from flash block x to y
    ATERx,y       erase flash rom from block x to y
    ATWFx,y,z     copy data from addr x to flash addr y, length z
    ATLOa,b,c,d   Int/Trap Log Cmd

    OK

Booting uCLinux

Please note: The following text is slightly out-of-date. In 2.4.24-uc0, a patch from me was integrated that adds better support for the router. A description is here in uclinux CVS. It adds the "Support ZyXEL BootExtension" configuration option which you should check!

Build the kernel

First, you need the arm-elf toolchain, i.e. the gcc and binutils compiled for the ARM target. I succeeded with prebuilt arm-elf-tools-20030314.sh from http://www.uclinux.org/pub/uClinux/arm-elf-tools/.

For the kernel, take a plain Linux kernel (I tried 2.4.22) and apply the matching uCLinux patch - available from http://www.uclinux.org/pub/uClinux/uClinux-2.4.x/.

To build a kernel for the router, a few minor changes have to be made in the kernel source tree. First, uncomment these lines in the top-level Makefile to declare that you're building a kernel for the "armnommu" architecture, and that you're using a cross-compiler:

   ARCH := armnommu
   CROSS_COMPILE = arm-elf-

We're going to build a "SNDS-100" kernel. That is the name of a well-documented development board that has the S3C4510 processor onboard. But a few definitions need to be changed to make it run on the router, because the router is a little different from the SNDS-100:

Note: To build a compressed image (zImage), more changes are required because the Makefiles in arch/armnommu/boot/compressed aren't properly prepared to produce big-endian code. Therefore we stay with an uncompressed image for now...

After making the above changes, go ahead and configure the kernel using "make menuconfig" or "make xconfig". For the first try, use the following settings:

  Code maturity options:
     [X] Prompt for development and/or incomplete code/drivers
     [ ] Prompt for obsolete code/drivers
  Loadable module support:
     [ ] Enable loadable module support
  System type:
     (Samsung) ARM system type
     [X] Generate big endian code
     [ ] Set flash/sdram size and base addr
     (RAM) Kernel executes from
     (S3C4510-SNDS100) Board Implementation
  Character devices:
     [X] Samsung serial port support
     [X]  Support for console on Samsung serial port

Do a "make clean dep Image". The result hopefully is a file arch/armnommu/boot/Image. This is the kernel (uncompressed) that can now be uploaded to the router. Determine its size in hex, so we can issue the correct upload command at the router later.

Now boot the router, enter Debug mode and enable the DebugFlag (see above). Issue the ATUP command to upload the kernel at address 0x20000. Assuming the Image file size is 429540 bytes, i.e. 0x68DE4 bytes in hex, the command would be this:

    ATUP20000,68DE4

Once the Xmodem upload is complete, make sure you have the serial speed (baud rate) set to 19200 bps (e.g. by issuing the command ATBA2) (or whatever is configured in drivers/char/serial_samsung.c), and start the kernel at 0x20000:

    ATBA2
    Now, console speed will be changed to 19200 bps

    OK
    ATGO20000

    Linux version 2.4.22-uc0 (kawk@n5) (gcc version 2.95.3 20010315 (release)(ColdFire patches - 20010318 from http://fiddes.net/coldfire/)(uClinux XIP and shared lib patches from http://www.snapgear.com/)) #1 Sun Dec 21 15:08:45 CET 2003
    Processor: Samsung S3C4510B revision 6
    Architecture: SNDS100
    On node 0 totalpages: 1024
    zone(0): 0 pages.
    zone(1): 1024 pages.
    zone(2): 0 pages.
    Kernel command line: root=/dev/rom0
    Calibrating delay loop... 49.86 BogoMIPS
    Memory: 4MB = 4MB total
    Memory: 3372KB available (365K code, 143K data, 28K init)
    Dentry cache hash table entries: 512 (order: 0, 4096 bytes)
    Inode cache hash table entries: 512 (order: 0, 4096 bytes)
    Mount cache hash table entries: 512 (order: 0, 4096 bytes)
    Buffer cache hash table entries: 1024 (order: 0, 4096 bytes)
    Page-cache hash table entries: 1024 (order: 0, 4096 bytes)
    POSIX conformance testing by UNIFIX
    Linux NET4.0 for Linux 2.4
    Based upon Swansea University Computer Society NET3.039
    Starting kswapd
    Samsung S3C4510 Serial driver version 0.9 (2001-12-27) with no serial options enabled
    ttyS00 at 0x3ffd000 (irq = 5) is a S3C4510B
    ttyS01 at 0x3ffe000 (irq = 7) is a S3C4510B
    Kernel panic: VFS: Unable to mount root fs on 00:00

Big Endian vs. Little Endian

My router is set to run in Big Endian mode (BE). This introduces a lot of problems, because support for Big Endian targets is still work in progress in uClinux for armnommu. Some of my patches to get this completely working have been integrated in uClinux 2.4 CVS in January 2004, especially regarding the checksumming code in these files:

   arch/armnommu/lib/csumpartial.S
   arch/armnommu/lib/csumpartialcopy.S
   arch/armnommu/lib/csumpartialcopygeneric.S
   arch/armnommu/lib/csumpartialcopyuser.S
   include/asm-armnommu/checksum.h

I'm yet working on patches for the s3c4510 ethernet driver - only two lines need change for Big Endian, but activation of the PHY also needs to be integrated and that'll take time until I have a clean patch:

   (s3c4510.c) FD_ptr->Reserved = (Padding | CRCMode | FrameDataPtrInc | WA00 | MACTxIntEn);
   (s3c4510.h) gBDMARXCON = BRxDIE | BRxEn | BRxMAINC | BRxBRST | BRxNLIE | BRxNOIE | BRxSTSKO | BRxWA10,

Network devices

The LSI80225 PHY for the LAN interface, driven by the S3C4510X's internal EMAC, has to explictly enabled by driving I/O pin #6 low (IOPMOD[6]=1 and IOPDATA[6]=0). Then, the drivers/net/s3c4510.c with small fixes for Big Endian mode will just run fine. Haven't done any stress tests however...

The second (WAN) network interface uses a RTL8019AS MAC with integrated PHY. This one has to be enabled by driving I/O pin #2 low (IOPMOD[2]=1 and IOPDATA[2]=0). Its interrupt is connected to the S3C4510X's external interrupt line #3; the setting IOPCON[19:15]=11101 is usable.

The RTL8019AS is connected in external I/O bank 1 using a bus width of 16 bit (EXTDBWTH[22:23]=10). If I/O base address was set to 0x3F00000 (REFEXTCON[9:0]=0x3F0), the registers appear starting at 0x3F045C0.

To match the big endian host, the MSB of the RTL8019AS data bus is connected to the LSB of the S3C4510X data bus. Therefore the RTL8019AS cannot be accessed with the S3C4510 bus width set to 8 bit (the LSB of RTL8019AS data, e.g. register contents, wouldn't be visible to the S3C4510).

In 16 bit mode however, the (byte-wide) registers appear in address space on half-word boundaries, not byte boundaries, and the mapping in the ne.c / 8390.c driver code has to be tweaked. The address on S3C4510 bus appears at the RTL8019AS shifted right by one; i.e. if you want to read a value from 0x2E0, you have to access 0x5C0 instead.

The LEDs

The TEST LED is controlled by I/O pin #1. It lights when IOPMOD[1]=1 and IOPDATA[1]=0. The other LEDs are probably hardwired to the respective output pins from the RTL8019AS, power supply, and WAN PHY.

Make a root filesystem etc.

This is on my to-do list. In the meantime, I decoded the various structures used by the BootExtension - see the following section. Here the intention was to become able to construct a valid firmware image, without ZyNOS RasCode but including my own kernel instead. The presented information should be sufficient to achieve this now.

ZyNOS BootExtension data

Memory map

The flash is visible in the ARM7's address space at 0x02000000 and also at 0x06000000. You can get a directory of its contents (as well as the proposed destinations in RAM) using the ATMP command:

    ROMIO image start at 06008000
    code version: 
    code start: 00000100
    code length: D6B84
    memMapTab: 15 entries, start = 02021000, checksum = B001
    $RAM Section:
      0: BootExt(RAMBOOT), start=00000100, len=17F00
      1: BootData(RAM),    start=00018000, len=8000
      2: HTPCode(RAMCODE), start=00020000, len=10000
      3: HTPData(RAM),     start=0003c000, len=14000
      4: RasCode(RAMCODE), start=00020000, len=1E0000
      5: RasData(RAM),     start=00200000, len=200000
    $ROM Section:
      6: BootBas(ROMIMG),  start=02000000, len=4000
      7: DbgArea(ROMIMG),  start=02004000, len=2000
      8: RomDir2(ROMDIR),  start=02006000, len=2000
      9: BootExt(ROMIMG),  start=02008030, len=10FD0
     10: HTPCode(ROMBIN),  start=02019000, len=8000
         (Compressed)
         Version: HTP_p310 V 0.10, start: 02019030
         Length: 828C, Checksum: 0A93
         Compressed Length: 4AD3, Checksum: EA0E
     11: MemMapT(ROMMAP),  start=02021000, len=C00
     12: termcap(ROMIMG),  start=02021c00, len=400
     13: RomDefa(ROMIMG),  start=02022000, len=2000
     14: RasCode(ROMBIN),  start=02024000, len=DC000
         (Compressed)
         Version: RAS p310, start: 02024030
         Length: 16BA58, Checksum: 6849
         Compressed Length: BAB84, Checksum: 097A
    $USER Section:
    ...

The first two sections in $ROM, i.e. BootBas and DbgArea, cannot be changed with the standard upload commands. ATLC targets the 2x8kByte DbgArea+RomDir2 at offset 0x4000, and ATUR puts the received data into flash starting at offset 0x8000.

Data structures

Firmware images, named "ROMIO image" in the output of ATMP, always begin with a 0x30 bytes header. It contains the information described in the following table. Note that numbers are stored big-endian, i.e. the most significant byte appears at the lowest address!

OffsetSize[bytes]Meaning
04Start address of boot code in RAM
42All zero (0)
63Three ascii chars "SIG"
91The value 0x03(?)
104Size[bytes] of data following this header
144All zero (0)
181Flags (0x40?)
202Expected checksum of data following this header
222All zero (0)
2415ASCIIZ Code version
394Absolute location of memMapTab memory map table
435All zero (0)

The memory map, which is parsed e.g. when you issue the ATMP command, consists of a 0x18 bytes header, followed by the table entries (each 0x18 bytes), and ASCII text for the "$USER" section (exactly what ATMP prints at the end following the $USER title). The header looks like this:

OffsetSize[bytes]Meaning
02Number of memMapTab entries following
24Location of start of $USER section
64Location behind end of $USER section
102Expected checksum of the memMapTab data following the header, including $USER section
1212All zero (0)

Each entry in the memMapTab describes an "object" somewhere in memory:

OffsetSize[bytes]Meaning
01Object type
18ASCIIZ Object name (e.g. BootBas)
91Zero (0)
104Object start address in memory
164Object size(bytes)
203All zero (0)
231Value 0x01 for DbgArea, 0x02 for RomDir2, 0x03 for BootExt - 0 otherwise(?)

When MSB is set in the type byte, this means the entry is within the $RAM section, otherwise it is for $ROM. This is a complete list of types as ATMP displays them:

TypeSectionName (as ATMP says)
1$ROMROMIMG
4$ROMROMBIN
5$ROMROMDIR
7$ROMROMMAP
128$RAMRAM
129$RAMRAMCODE
130$RAMRAMBOOT

Where ROMBIN entries point to, you'll find another 0x30 bytes header describing the data immediately following that header, as shown below. The Size given in the memMapTab for these entries specifies the space reserved for the data, which may be more than the actual size as specified in the header there:

OffsetSize[bytes]Meaning
06All zero (0)
63Three ascii chars "SIG"
91Object type (0x04 for ROMBIN)
104Size[bytes] of uncompressed data
144Size[bytes] of compressed data
181Flags (0xE0?)
202Expected checksum of uncompressed data
222Expected checksum of compressed data
2415ASCIIZ Code version
399All zero (0)

If the flags have bit 7 set (0x80), this means the following data is stored in compressed form. Compressed data is stored as contiguous chunks of LZW encoded data; each starting with a halfword (16 bit) specifying the size of the chunk when uncompressed (2048 except for the last chunk), followed by a halfword specifying the size of the compressed data in the chunk, and finally followed by the compressed data itself. If you concatenate the data parts of all chunks, you can decompress them using the LZW demo code from Mark Nelson. And that code also would allow you to compress your own data to build your own firmware...

Note for the future: I took a look at firmware of a newer device, the ZyAIR B-2000 V.2. The firmware still uses the same structure with HTPCode and RasCode and memMapTable etc., albeit compressed data there is not any longer stored LZW encoded as described above, but simply compressed with bzip2. You can run bunzip2 immediately on the data following the SIG header to get the uncompressed code.

Finally, while looking at all the data, I found the info at the end of the BootBase area that defines some of the info displayed by the ATSH command:

Offset (hex)Size[bytes]Meaning
0x3F7221SNMP MIB level & OID
0x3F871Zero (0)
0x3F888Values 0x00020000 and 0x00100000(?)
0x3F9032ASCIIZ Vendor name
0x3FB032ASCIIZ Product Model
0x3FD04RAS ROM address
0x3FD42Zero (0)
0x3FD610x05 (System type? Flash type?)
0x3FD71Zero (0)
0x3FD822Other feature bits
0x3FF61Main feature bits
0x3FF71Zero (0)
0x3FF86MAC Address
0x3FFE1Default Country Code
0x3FFF1BootExtension DebugFlag

Computing checksums

The following code yields correct checksums as used in the ROMIO, ROMBIN and memMapTab structures (might be written prettier, but this is similar to what the BootExtension code actually does):

long compute_checksum(long length)
{
  int n;
  long checked;
  unsigned long sum;

  for(sum=0, checked = 0; checked < (length&~1); checked+=2)
  {
    sum += GET_NEXT_BYTE << 8;
    sum += GET_NEXT_BYTE;
    if(sum > 0xFFFF) sum = (sum+1)&0xFFFF;
  };

  if(length&1)
  {
    sum += GET_NEXT_BYTE << 8;
    if(sum > 0xFFFF) sum = (sum+1)&0xFFFF;
  };

  return sum;
}

Hardware notes

2-pin header J4

This connects I/O "p0" of S3C4510 to ground. When connected, the router at startup doesn't ask to press "any" key to enter Debug Mode, but to press "ESC" key. More important is that it starts HTPCode (that is the Hardware Test Program) afterwards instead of RasCode (the router application).

2x7-pin header JP1

That's a standard ARM JTAG header. This is its pinout:

JTAG signalPin No.Pin No.JTAG signal
TVcc (3.3V)12GND
nTRST34GND
TDI56GND
TMS78GND
TCK910GND
TDO1112nRESET (not connected!)
TVcc1314GND

As in the SNDS100 reference board circuit, nRESET at the JTAG header isn't connected to anything, but the nTRST causes both nTRST and nRESET to be asserted. Thus if you own a router without RESET button, you could add one yourself (short nTRST and GND).

Unfortunately this means that asserting nTRST too long would cause a full system reset. I ran into problems with this when I tried the openwince-jtag tools. The armtool from Erwin Authried (midori Linux distribution) works better and I was able to upload an Image to the router with it (3 times faster than with a 115200 bps serial Xmodem upload), but it ended up having the bytes reversed (again: little vs. big endian hassles...)

2x6-pin header S1

This probably could be populated with a switch to change the wiring on the LAN port, to emulate a cross-connected Ethernet cable.

I/O pin assignments

These have been mentioned in the text above already, but I'll summarize them in the following table. IOPMOD and IOPDATA registers are used to access the I/O pins.

I/O PinDirectionMeaning
0InEnable HTP at bootup (connected to header S4)
1OutWhen low, TEST LED is lit
2Out0 = Enable WAN interface (RTL8019AS)
6Out0 = Enable LAN PHY (LSI80225)

ZyNOS turns all pins #0..#7 into outputs after booting.