Flashing a Windows CE 5.0 Device Without Using Platform Builder

 

Rob Baynes
Koko Fitness, Inc.

April 2006

Applies to Windows CE 5.0

Summary: This article describes how to build a Windows CE Device that flashes without using Platform Builder.

I work at a small startup, Koko Fitness, Inc., that builds fitness equipment. We built a custom Windows CE 5.0 device for our gym that uses a customized Windows CE operating system and drivers. We outsourced the manufacture and assembly of the device and we needed a simple way for an assembly line worker to flash the device without using Platform Builder.

To run the code in this article, you will need the following bits installed on your machine:

  • Platform Builder with Windows CE 5.0 and your BSP (board support package)
  • Visual Studio 2005 with C#
  • A network analyzer, such as windump (which is free)

These are the manual steps involved in flashing a custom CE device:

1. Connect a null modem serial cable from your PC to the devices serial port. Run the download program that comes with your BSP. Use it to program the MAC address and serial number into EEPROM.

Typically it's used like this:

a. Run:

download -a 0016EF000001  -n C12303001

b. Hold down a boot switch and turn on the device.

c. Turn the device off after downloading is complete.

2. Run the download program again to download the Ethernet boot loader.

Typically it's used like this:

a. Run:

download EBOOT.nb0

b. Hold down a boot switch and turn on the device.

c. Turn the device off after downloading is complete.

3. These are the steps to flash the image to the device:

a. Connect an Ethernet cable to the device.

b. Start Platform Builder and open your workspace project.

c. Turn on the device.

d. In Platform Builder, click Target, click Connectivity Options, and then click Settings. You should see your devices IP address (assigned by DHCP) under the Active Devices list.

e. Click Target, and then click Attach Device, which will download the image and flash the device.

f. Wait a few minutes for the flashing to complete then turn the device off and on to see your image boot.

If I worked on an assembly line, I don't think I could do those steps a hundred times in a day without making a mistake. So I looked for a way to simplify the process. My first step was to figure out how Platform Builder discovered the device's IP address. I went back to my board and did step 1 above. Then I ran the network analyzer and looked for traffic from the board. I narrowed down the packets on my network, and saw some interesting UDP packets that were unicast to port 980. Here is a dump of a packet the Ethernet boot loader unicasts:

windump -i 2 -X port 980
1 c. 4:01:25.525684 IP 192.168.1.107.980 > 255.255.255.255.980: UDP, length 64
        0x0000:  4500 005c 0600 0000 4011 b27e c0a8 016b  E..\....@..~...k
        0x0010:  ffff ffff 03d4 03d4 0048 bd19 4544 4247  .........H..EDBG
        0x0020:  ff01 0400 0100 0016 ef00 0001 c0a8 016b  ...............k
        0x0030:  4b6f 6b6f 2d43 3100 0000 0000 0000 0000  Koko-C1.........
        0x0040:  004b 6f6b 6f2d 4331 3233 3033 3030 3100  .Koko-C12303001.
        0x0050:  0000                                     ..

Digging into the packet reveals the following data:

Offset Data
0x1C to 0x1F 45444247
0x26 to 0x2B 0016ef000001
0x2C to 0x2F c0a8016b
0x41 to 0x4E 4b6f6b6f2d433132333033303031

From the information above, I can tell that Platform Builder must be listening for these UDP packets on port 980 to get the device's IP address. Now, how does Platform Builder download the CE operating system image to the device over Ethernet? For clues I looked at the Ethernet boot loader source code. If you navigate to where you installed the Windows CE 5.0 files (usually under the same directory where your platform builder project is), you should be able to find this:

file: public\common\oak\ethdbg\eboot\ebsimp.c

On about line 300, you will see a call to a function called EbootInitTftpSimple(…) which starts a TFTP server, but on a non-standard port (980). A few lines further down you can also see where the packet above is unicast. There is a tftp.exe client that comes with Windows XP and later, but it operates on the standard port of 69. So, it's trivial to modify the call to the function to use port 69, instead of using the define EDBG_DOWNLOAD_PORT (which is 980).

Here is the original code in ebsimp.c:

    if( !EbootInitTftpSimple( pEdbgAddr, htons( EDBG_DOWNLOAD_PORT ), 
                              htons( EDBG_DOWNLOAD_PORT ),
                              EDBG_DOWNLOAD_FILENAME )) {
        return FALSE;
    }

Here is my modified version:

    // rbaynes: change port EDBG_DOWNLOAD_PORT to 69, to allow
    // windows command line tftp.exe to send us an image (also prevents
    // Platform Builder from sending image)    
    if( !EbootInitTftpSimple( pEdbgAddr, htons(69), htons(69),  
                              EDBG_DOWNLOAD_FILENAME )) {
        return FALSE;
    }

A few things to note:

  • I didn't change the port that the packet is unicast on (its still 980). All the above change does is make the TFTP server in the boot loader listen on the standard TFTP port of 69.
  • The TFTP server is waiting for a file named "boot.bin" (defined as EDBG_DOWNLOAD_FILENAME), it won't accept any other file names. So, you will have to rename the file that platform builder creates (NK.bin) to "boot.bin."
  • You may want to save your old boot loader, in case you still want to download images to your device using Platform Builder. That is why I named the custom boot loader we created "EBOOT.nb0.tftp_port_69."

Use Platform Builder to rebuild your project, which should also rebuild the boot loader. You can find the boot loader (EBOOT.nb0) and Windows CE image (NK.bin) in a similar directory when the build is complete: OS\PBWorkspaces\Koko\RelDir\ep93xx_ARMV4I_Release.

Now that we know how everything works, it's time to write a simple-to-use application that will:

  1. Run download.exe to program the MAC address and serial number, prompting the user to press the boot switch and turn the board power on and off.
  2. Run download.exe to download our modified boot loader, prompting the user to press the boot switch and turn the board power on and off.
  3. Prompt the user to turn the board on.
  4. Capture the UDP packet that is unicast to port 980, and extract the IP address from it.
  5. Run tftp.exe to send the CE image to the boards IP address.

In the screen shot, you can see what my application looks like. The two text fields are filled in using a scanner that the assembly line worker scans from a work order sheet for this device. In the output window at the bottom, you can see that the program did not find the three files it needs. The application prompts the user through the three steps.

Aa446908.flashingdevice01(en-us,MSDN.10).gif

Screenshot of the customized Windows CE device UI

I wrote the application above in C#. I'm only going to cover the difficult parts of the application, since writing Windows Forms applications is already well documented in MSDN. The difficult parts are capturing and decoding the UDP packet and spawning the processes which run the command line programs.

This is the code I use to spawn a process and capture its output (I look at the output to see if the program worked):

    using System.Threading;
    try {
        // run: download -a 0016EF000001 -n C12303001
        // expect: "Successfully programmed the Ethernet MAC address."
        Process p = new Process();
        p.EnableRaisingEvents = false;
        p.StartInfo.UseShellExecute = false;
        p.StartInfo.RedirectStandardError = true;
        p.StartInfo.RedirectStandardOutput = true;
        p.StartInfo.CreateNoWindow = true;
        p.StartInfo.FileName = downloadApp;
        p.StartInfo.Arguments = " -a " + MACadxBox.Text + 
                                " -n " + SNBox.Text;
        p.Start();
        string stderr = p.StandardError.ReadToEnd();
        string stdout = p.StandardOutput.ReadToEnd();
        p.Close();

        if( 0 > stdout.IndexOf( "successful", 
                        StringComparison.CurrentCultureIgnoreCase )) {
            MessageBox.Show( "Error: please review the messages and " +
                             "try again." );
            return;
        }
    } catch( Exception ex ) {
        Utils.Log( "Exception: " + ex.Message );
    }

Here is the complete listing for the class which captures and decodes the packet the boot loader sends:

// copyright (c) 2006, Koko Fitness, Inc.

// read the UDP BOOTME packet our Windows CE bootloader unicasts
// to 255.255.255.255 port 980

using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Threading;
using System.Net.Sockets;
using System.Net;

namespace Imager
{
    class UDPclient
    {
        private int listenOnPort = 980; // port boot loader unicasts on
        private int messageCount = 1;   // just read 1 packet
        private int listenWait = 60;
        private const int bufSize = 32 * 1024 - 40;
        private byte[] receiveBuffer1 = new byte[ bufSize ];
        private int receivedBuffers = 0;
        private bool success = false;
        private string errorMsg = "";
        private Socket socketA1 = null;
        EndPoint bindEndPointA1 = null;

        private string MAC_adx = "";
        private string IP_adx = "";
        private string board_name = "";

        //-------------------------------------------------------------
        public bool Success {
            get {
                return success;
            }
        }
        public string ErrorMsg {
            get {
                return errorMsg;
            }
        }
        public string MAC {
            get {
                return MAC_adx;
            }
        }
        public string IP {
            get {
                return IP_adx;
            }
        }
        public string Name {
            get {
                return board_name;
            }
        }

        //-------------------------------------------------------------
        public UDPclient()
        {
            try {
                // init our buffer
                for( int i = 0; i < bufSize; i++ ) {
                    receiveBuffer1[ i ] = 0;
                }

                AsyncCallback onReceiveFrom1 = 
                    new AsyncCallback( OnReceiveFrom );

                if( null == socketA1 ) {
                    socketA1 = new Socket( AddressFamily.InterNetwork,
                        SocketType.Dgram, ProtocolType.Udp );
                    bindEndPointA1 = new IPEndPoint( IPAddress.Any,
                        listenOnPort );
                    // bind for listening to unicast.
                    socketA1.Bind( bindEndPointA1 );
                }

                // start listening
                ReceiveFromData rfd = new ReceiveFromData( 
                    ref receiveBuffer1,
                    receiveBuffer1.Length, ref bindEndPointA1, 
                    ref onReceiveFrom1, ref socketA1, null );
                rfd.BeginReceiveFrom();

                WaitReceivedMessages( messageCount + 1 );

                // clean up
                socketA1.Disconnect( true );
                socketA1.Close();

            } catch( Exception ex ) {
                errorMsg = "Exception: " + ex.ToString();
                socketA1.Disconnect( true );
                socketA1.Close();
            }
        }
        ~UDPclient()
        {
            try {
                socketA1.Disconnect( true );
                socketA1.Close();
            } catch( Exception ) {
            }
        }

        //-------------------------------------------------------------
        private void OnReceiveFrom( IAsyncResult result )
        {
            ReceiveFromData rfd = (ReceiveFromData) result.AsyncState;
            Socket receiveSocket = rfd.socket;
            EndPoint remoteEndPoint;
            remoteEndPoint = new IPEndPoint( 0, 0 ); // hack
            int bytesRead = 0;

            try {
                bytesRead = receiveSocket.EndReceiveFrom( result, 
                    ref remoteEndPoint );
            } catch( SocketException e ) {
                errorMsg += "SocketException: " + e.ErrorCode;
                switch( e.ErrorCode ) {
                case 995: 
                    break; // thread terminated, expected
                default:
                    errorMsg += "SocketException: " + 
                        e.ErrorCode + " " + e.ToString();
                    break;
                }
            } catch( Exception e ) {
                errorMsg += "Exception: " + e.ToString();
            }

// now see if this is the 64 byte packet we expect:
//
// description  windump       buffer   data (hex)    translated  
// -----------  ------------  -------  -----------   ---------- 
// packet name  0x1C to 0x1F  0 - 3    45444247      EDBG     
// MAC address  0x26 to 0x2B  10 - 15  0016ef000001  00-16-ef-00-00-01
// IP address   0x2C to 0x2F  16 - 19  c0a8016b      192.168.1.107    
// board name   0x41 to 0x4E  37 - 50  4b6f6b6f2...  Koko-C12303001 

            if( 64 == bytesRead &&             // expected packet size
                0x45 == rfd.buffer[ 0 ] &&     // E
                0x44 == rfd.buffer[ 1 ] &&     // D
                0x42 == rfd.buffer[ 2 ] &&     // B
                0x47 == rfd.buffer[ 3 ] ) {    // G

                success = true;
                MAC_adx = "";
                IP_adx = "";
                board_name = "";

                MAC_adx += System.Convert.ToInt32( 
                    rfd.buffer[ 10 ] ).ToString( "X2" )+
                    "-";
                MAC_adx += System.Convert.ToInt32( 
                    rfd.buffer[ 11 ] ).ToString( "X2" )+
                    "-";
                MAC_adx += System.Convert.ToInt32( 
                    rfd.buffer[ 12 ] ).ToString( "X2" )+
                    "-";
                MAC_adx += System.Convert.ToInt32( 
                    rfd.buffer[ 13 ] ).ToString( "X2" )+
                    "-";
                MAC_adx += System.Convert.ToInt32( 
                    rfd.buffer[ 14 ] ).ToString( "X2" )+
                    "-";
                MAC_adx += System.Convert.ToInt32( 
                    rfd.buffer[ 15 ] ).ToString( "X2" );
                MAC_adx += "";

                IP_adx += 
                    System.Convert.ToInt32( rfd.buffer[ 16 ] ).ToString() +
                    ".";
                IP_adx += 
                    System.Convert.ToInt32( rfd.buffer[ 17 ] ).ToString() +
                    ".";
                IP_adx += 
                    System.Convert.ToInt32( rfd.buffer[ 18 ] ).ToString() +
                    ".";
                IP_adx += 
                    System.Convert.ToInt32( rfd.buffer[ 19 ] ).ToString();
                IP_adx += "";

                board_name += System.Convert.ToChar( rfd.buffer[ 37 ] );
                board_name += System.Convert.ToChar( rfd.buffer[ 38 ] );
                board_name += System.Convert.ToChar( rfd.buffer[ 39 ] );
                board_name += System.Convert.ToChar( rfd.buffer[ 40 ] );
                board_name += System.Convert.ToChar( rfd.buffer[ 41 ] );
                board_name += System.Convert.ToChar( rfd.buffer[ 42 ] );
                board_name += System.Convert.ToChar( rfd.buffer[ 43 ] );
                board_name += System.Convert.ToChar( rfd.buffer[ 44 ] );
                board_name += System.Convert.ToChar( rfd.buffer[ 45 ] );
                board_name += System.Convert.ToChar( rfd.buffer[ 46 ] );
                board_name += System.Convert.ToChar( rfd.buffer[ 47 ] );
                board_name += System.Convert.ToChar( rfd.buffer[ 48 ] );
                board_name += System.Convert.ToChar( rfd.buffer[ 49 ] );
                board_name += System.Convert.ToChar( rfd.buffer[ 50 ] );
                board_name += "";

            } else {
                // get another packet
                rfd.BeginReceiveFrom();
                Interlocked.Increment( ref receivedBuffers );
            }
        }

        //-------------------------------------------------------------
        private class ReceiveFromData
        {
            public AsyncCallback onReceiveFrom;
            public byte[] buffer;
            public int length;
            public EndPoint endPoint;
            public Socket socket;
            public Socket ForwardSocket;
            public ReceiveFromData(
                ref byte[] buffer, int length, ref EndPoint endPoint,
                ref AsyncCallback onReceiveFrom, ref Socket socket, 
                Socket ForwardSocket )
            {
                this.onReceiveFrom = onReceiveFrom;
                this.buffer = buffer;
                this.length = length;
                this.endPoint = endPoint;
                this.socket = socket;
                this.ForwardSocket = ForwardSocket;
            }

            public void BeginReceiveFrom()
            {
                socket.BeginReceiveFrom( buffer, 0, length, 
                    SocketFlags.None, ref endPoint, onReceiveFrom, 
                    (object) this );
            }
        }

        //-------------------------------------------------------------
        private bool WaitReceivedMessages( int messages )
        {
            for( int i = 0; i < listenWait * 10; i++ ) {
                Thread.Sleep( 100 );
                if( receivedBuffers == messages )
                    break;
            }
            return ( receivedBuffers == messages );
        }
    }
}