Bar
SpaceWire UK
Specialist providers of VHDL Intellectual Property & Design Services
BarBarBarBar
Tutorial
Missing Image!
Part 19 - Add on-the-fly PL firmware load capability & enhance the peek/poke address table

Introduction

This tutorial details the steps required to load a new firmware image stored on the SD Card without the need to reboot PetaLinux. The Peek/Poke address table will also be enhanced to include a few new features (range widget, select widget, individual hex/dec display & copy section). Migration from JTAG load to SD Card load will be adapted from here on in so the SD Card can be used to provide extra files (firmware images in this case) to PetaLinux. The tutorial covers quite a range of activities and includes the use of numerous languages; C, HTML & Javascript.

Aims

The aims of this tutorial are as follows :-

    Part 1 - Project Setup

    1. Setup environment
    2. Obtain tutorial files from Bitbucket (optional)
    3. Change present working directory
    4. Bump Version

    Part 2 - OS Development

    1. Fix issue with Apache .bbappend
    2. Package project to produce boot image
    3. Copy files to SD Card
    4. Deploy on Zedboard via SD Card
    5. Launch terminal emulator

    Part 3 - Software Development (Load Firmware)

    1. Create load firmware C application
    2. Cross compile application
    3. Copy application to PetaLinux
    4. Create loadable binary for bitstream
    5. Check application is working as expected
    6. Modify Makefile & BitBake recipe

    Part 4 - Website Development (Load Firmware)

    1. Update HTML
    2. Update Javascript
    3. Check everything is working as expected

    Part 5 - Revisions Control

    1. Commit new & updated files

    Part 6 - Final checks

    1. Create a Production Release
    2. Deploy Production Release on the Zedboard
    3. Check everything is working as expected
    #### Part 1 - Project Setup ####

    1. Setup environment

    Setup Xilinx design environment for the 2021.2 toolset.
    steve@Desktop:~$ xilinx
    Xilinx tools available tools at /opt/Xilinx :-
    1) 2021.2 - Vivado - SDK - Vitis - PetaLinux
    0) Exit
    Please select tools required or exit : 1
    
    Tools are as follows :-
    vivado @ /opt/Xilinx/Vivado/2021.2/bin/vivado
    vitis @ /opt/Xilinx/Vitis/2021.2/bin/vitis
    petalinux-build @ /opt/Xilinx/PetaLinux/2021.2/tool/tools/common/petalinux/bin/petalinux-build
    

    2. Obtain tutorial files from Bitbucket (optional)

    Starting from this point, having not followed the previous tutorials, can be achieved by obtaining the required files from BitBucket. The only prerequisite is that Part 1 - Installation of tools, setup of environment and creation of project area as been completed.

    Obtain OS source.
    steve@Desktop:~$ cd ~/projects
    steve@Desktop:~/projects$ git clone -b v12.0 https://bitbucket.org/spacewire_firmware/zedboard_linux
    

    3. Change present working directory

    Change the present working directory to be the project directory.
    steve@Desktop:~$ cd ~/projects/zedboard_linux
    

    4. Bump Version

    Change the version (or revision) number for this new development, this prevents ghost (post-release, same version) builds from appearing.
    steve@Desktop:~/projects/zedboard_linux$ sed -i 's/12.0/13.0/g' os/petalinux/project-spec/meta-user/recipes-apps/website/files/project.txt
    

    project.txt

    Zedboard PetaLinux Example Design
    SpaceWire UK
    Steve Haywood
    13.0
    
    Direct download available here :-
    steve@Desktop:~/projects/zedboard_linux$ wget https://spacewire.co.uk/tutorial/shared/repos/0025/zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/project.txt -O os/petalinux/project-spec/meta-user/recipes-apps/website/files/project.txt
    
    #### Part 2 - OS Development ####

    5. Fix issue with Apache .bbappend

    The current location of the apache2_%.bbappend breaks the PetaLinux build process given a fresh clone of the repo (v12.0 tag). Moving this file to a more suitable location fixes the issue.
    steve@Desktop:~/projects/zedboard_linux$ cd os/petalinux
    steve@Desktop:~/projects/zedboard_linux/os/petalinux$ mkdir -p project-spec/meta-user/recipes-httpd/apache2
    steve@Desktop:~/projects/zedboard_linux/os/petalinux$ git mv components/yocto/layers/meta-openembedded/meta-webserver/recipes-httpd/apache2/apache2_%.bbappend project-spec/meta-user/recipes-httpd/apache2/apache2_%.bbappend
    
    If step 2 was followed then execute the following to remove the stay directory & also to build PetaLinux.
    steve@Desktop:~/projects/zedboard_linux/os/petalinux$ rm -r components
    steve@Desktop:~/projects/zedboard_linux/os/petalinux$ petalinux-build
    

    6. Package project to produce boot image

    Package the project to produce the boot image BOOT.BIN, this will include the first stage boot loader zynq_fsbl.elf, the programmable logic system.bit, the Linux boot loader u-boot.elf and the device tree blob system.dtb.
    steve@Desktop:~/projects/zedboard_linux/os/petalinux$ petalinux-package --boot --force --fsbl images/linux/zynq_fsbl.elf --fpga images/linux/system.bit --uboot images/linux/u-boot.elf
    

    7. Copy files to SD Card

    Mount a FAT32 formatted SD card on the Xubuntu desktop and copy the required files onto it, then unmount the card so that it is ready for use on the Zedboard.
    steve@Desktop:~/projects/zedboard_linux/os/petalinux$ cp /tftpboot/{BOOT.BIN,boot.scr,image.ub} /media/steve/petalinux
    steve@Desktop:~/projects/zedboard_linux/os/petalinux$ cd ../..
    

    8. Deploy on Zedboard via SD Card

    If not already, connect up the hardware as follows :-
    1. Xubuntu PC USB ⇄ Zedboard USB JTAG/Debug
    2. Xubuntu PC USB ⇄ Zedboard USB UART
    3. Zedboard Ethernet ⇄ Router
    4. Xubuntu PC Ethenet ⇄ Router
    5. Router ⇄ Internet
    Missing Image!Set the boot mode jumpers on the Zedboard for SD Card.Missing Image!Power on the Zedboard.

    9. Launch terminal emulator

    If not already running, open up a new terminal and launch the MiniCom terminal emulator.
    steve@Desktop:~$ minized
    
    Welcome to minicom 2.7.1
    
    OPTIONS: I18n
    Compiled on Dec 23 2019, 02:06:26.
    Port /dev/ttyACM0, 06:34:25
    
    Press CTRL-A Z for help on special keys
    
    #### Part 3 - Software Development (Load Firmware) ####

    10. Create load firmware C application

    Create a new C application to access the FPGA Manager device driver and inform it to load a new bitstream. The FPGA Manager looks for bitstream images in the /lib/firmware directory, however it would be advantageous to load these from the SD Card. This can be achieved by simply creating a symbolic link, done within the C application.
    steve@Desktop:~/projects/zedboard_linux$ subl os/petalinux/project-spec/meta-user/recipes-apps/peekpokecgi/files/loadfirmware.c
    

    loadfirmware.c

    //
    // File .......... loadfirmware.c
    // Author ........ Steve Haywood
    // Website ....... http://www.spacewire.co.uk
    // Project ....... SpaceWire UK Tutorial
    // Version ....... 1.0
    // Conception .... 9 February 2024
    // Standard ...... C17 (ISO/IEC 9899:2018)
    // Description ...
    //   Very simple CGI to load PL bitstream.
    //
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <string.h>
    
    int main()
    {
      char *query;
      char *firmware = "/sys/class/fpga_manager/fpga0/firmware";
      char *target = "/lib/firmware";
      int   fd;
    
      int sl = symlink("/media/sd-mmcblk0p1/firmware", "/lib/firmware");
    
      printf("Content-Type: text/plain\n\n");
    
      query = getenv("QUERY_STRING");
      if (query) {
        fd = open(firmware, O_WRONLY);
        if (fd >= 0) {
          write(fd, query, strlen(query));
          close(fd);
        } else printf("Error: Failed to open %s", firmware);
      } else printf("Error: No QUERY_STRING");
    }
    
    Direct download available here :-
    steve@Desktop:~/projects/zedboard_linux$ wget https://spacewire.co.uk/tutorial/shared/repos/0025/zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/peekpokecgi/files/loadfirmware.c -O os/petalinux/project-spec/meta-user/recipes-apps/peekpokecgi/files/loadfirmware.c
    

    11. Cross compile application

    Cross compile the C code targetting the ARM processor.
    steve@Desktop:~/projects/zedboard_linux$ arm-linux-gnueabihf-gcc os/petalinux/project-spec/meta-user/recipes-apps/peekpokecgi/files/loadfirmware.c -o /tmp/loadfirmware
    

    12. Copy application to PetaLinux

    Copy the compiled application over to PetaLinux.
    steve@Desktop:~/projects/zedboard_linux$ ssh-keygen -f /home/steve/.ssh/known_hosts -R 192.168.2.87
    steve@Desktop:~/projects/zedboard_linux$ ssh-keyscan -H 192.168.2.87 >> ~/.ssh/known_hosts
    steve@Desktop:~/projects/zedboard_linux$ sshpass -p root scp /tmp/loadfirmware root@192.168.2.87:/srv/www/cgi-bin
    

    13. Create loadable binary for bitstream

    Create a bitstream only binary (.bin) to include the zedboard_leds_switches bitstream (.bit). To achieve this a Boot Image File (.bif) is required.
    steve@Desktop:~/projects/zedboard_linux$ cd /tmp
    steve@Desktop:/tmp$ printf 'all:\n{\n  system_wrapper.bit\n}' > system_wrapper.bif
    

    system_wrapper.bif

    1. all:
    2. {
    3.   system_wrapper.bit
    4. }
    Copy the bitstream and generate the binary.
    steve@Desktop:/tmp$ cp ~/projects/zedboard_leds_switches/fw/vivado/project.runs/impl_1/system_wrapper.bit .
    steve@Desktop:/tmp$ bootgen -arch zynq -image system_wrapper.bif -process_bitstream bin -w
    steve@Desktop:/tmp$ cd -
    
    Create a firmware directory on the SD Card to hold the loadable firmware images and copy over the newly created binary
    steve@Desktop:~/projects/zedboard_linux$ ssh-keygen -f /home/steve/.ssh/known_hosts -R 192.168.2.87
    steve@Desktop:~/projects/zedboard_linux$ ssh-keyscan -H 192.168.2.87 >> ~/.ssh/known_hosts
    steve@Desktop:~/projects/zedboard_linux$ sshpass -p root ssh -t root@192.168.2.87 'mkdir /media/sd-mmcblk0p1/firmware'
    steve@Desktop:~/projects/zedboard_linux$ sshpass -p root scp /tmp/system_wrapper.bit.bin root@192.168.2.87:/media/sd-mmcblk0p1/firmware/zedboard_leds_switches.bin
    

    14. Check application is working as expected

    Change the execute permissions on the loadfirmware application so it executes with root privileges (not recommended).
    steve@Desktop:~/projects/zedboard_linux$ ssh-keygen -f /home/steve/.ssh/known_hosts -R 192.168.2.87
    steve@Desktop:~/projects/zedboard_linux$ ssh-keyscan -H 192.168.2.87 >> ~/.ssh/known_hosts
    steve@Desktop:~/projects/zedboard_linux$ sshpass -p root ssh -t root@192.168.2.87 'chmod +s /srv/www/cgi-bin/loadfirmware'
    
    Using curl enter the following URL to command the poke CGI to write 0x18 to the LED register at 0x40010000. This should light up the two middle LEDs.
    steve@Desktop:~/projects/zedboard_linux$ curl -w "\nReceived %{size_download} bytes\n" "http://192.168.2.87/cgi-bin/poke?0x40010000&0x18"
    
    Using curl enter the following URL to command the loadfirmware CGI to load the firmware.
    steve@Desktop:~/projects/zedboard_linux$ curl -w "\nReceived %{size_download} bytes\n" "http://192.168.2.87/cgi-bin/loadfirmware?zedboard_leds_switches.bin"
    
    All being well the following message should be seen in the MiniCom terminal window and the two LEDs should be turned off (registers inside PL reset to firmware defaults).
    root@petalinux:~# fpga_manager fpga0: writing zedboard_leds_switches.bin to Xilinx Zynq FPGA Manager
    

    15. Modify Makefile & BitBake recipe

    With confidence that the load firmware application is working OK the Makefile & Bit Bake recipe can be modified to include the new application in the PetaLinux build process.
    steve@Desktop:~/projects/zedboard_linux$ subl os/petalinux/project-spec/meta-user/recipes-apps/peekpokecgi/files/Makefile
    

    Makefile

    PEEK = peek
    POKE = poke
    PEEKSTRING = peekstring
    LOADFIRMWARE = loadfirmware
    
    # Add any other object files to this list below
    PEEK_OBJS = peek.o
    POKE_OBJS = poke.o
    PEEKSTRING_OBJS = peekstring.o
    LOADFIRMWARE_OBJS = loadfirmware.o
    
    all: $(PEEK) $(POKE) $(PEEKSTRING) $(LOADFIRMWARE)
    
    $(LOADFIRMWARE): $(LOADFIRMWARE_OBJS)
    	$(CC) $(LDFLAGS) -o $@ $(LOADFIRMWARE_OBJS) $(LDLIBS)
    
    $(PEEKSTRING): $(PEEKSTRING_OBJS)
    	$(CC) $(LDFLAGS) -o $@ $(PEEKSTRING_OBJS) $(LDLIBS)
    
    $(POKE): $(POKE_OBJS)
    	$(CC) $(LDFLAGS) -o $@ $(POKE_OBJS) $(LDLIBS)
    
    $(PEEK): $(PEEK_OBJS)
    	$(CC) $(LDFLAGS) -o $@ $(PEEK_OBJS) $(LDLIBS)
    
    clean:
    	-rm -f $(LOADFIRMWARE) $(PEEKSTRING) $(POKE) $(PEEK) *.elf *.gdb *.o
    
    Direct download available here :-
    steve@Desktop:~/projects/zedboard_linux$ wget https://spacewire.co.uk/tutorial/shared/repos/0025/zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/peekpokecgi/files/Makefile -O os/petalinux/project-spec/meta-user/recipes-apps/peekpokecgi/files/Makefile
    
    Check out the changes.
    steve@Desktop:~/projects/zedboard_linux$ git difftool os/petalinux/project-spec/meta-user/recipes-apps/peekpokecgi/files/Makefile
    
    steve@Desktop:~/projects/zedboard_linux$ subl os/petalinux/project-spec/meta-user/recipes-apps/peekpokecgi/peekpokecgi.bb
    

    peekpokecgi.bb

    #
    # This is the peekpokecgi aplication recipe
    #
    #
    
    SUMMARY = "peekpokecgi application"
    SECTION = "PETALINUX/apps"
    LICENSE = "MIT"
    LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"
    SRC_URI = "file://peek.c \
               file://poke.c \
               file://peekstring.c \
               file://loadfirmware.c \
               file://Makefile \
              "
    FILES_${PN} += "/srv/www/cgi-bin"
    S = "${WORKDIR}"
    CFLAGS_prepend = "-I ${S}/include"
    do_compile() {
            oe_runmake
    }
    do_install() {
            install -d ${D}/srv/www/cgi-bin
            install -m 0755 ${S}/peek ${D}/srv/www/cgi-bin
            install -m 0755 ${S}/poke ${D}/srv/www/cgi-bin
            install -m 0755 ${S}/peekstring ${D}/srv/www/cgi-bin
            install -m 0755 ${S}/loadfirmware ${D}/srv/www/cgi-bin
            chmod a+s ${D}/srv/www/cgi-bin/peek
            chmod a+s ${D}/srv/www/cgi-bin/poke
            chmod a+s ${D}/srv/www/cgi-bin/peekstring
            chmod a+s ${D}/srv/www/cgi-bin/loadfirmware
    }
    
    Direct download available here :-
    steve@Desktop:~/projects/zedboard_linux$ wget https://spacewire.co.uk/tutorial/shared/repos/0025/zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/peekpokecgi/peekpokecgi.bb -O os/petalinux/project-spec/meta-user/recipes-apps/peekpokecgi/peekpokecgi.bb
    
    Check out the changes.
    steve@Desktop:~/projects/zedboard_linux$ git difftool os/petalinux/project-spec/meta-user/recipes-apps/peekpokecgi/peekpokecgi.bb
    
    #### Part 4 - Website Development (Load Firmware) ####

    16. Update HTML

    Update the webpage to remove the global Number Format option and replace it with an individual option for each row of the peek/poke address table. Also include a table that displays the loadable firmware images as found in the firmware directory on the SD Card.
    steve@Desktop:~/projects/zedboard_linux$ subl os/petalinux/project-spec/meta-user/recipes-apps/website/files/cgi-bin/index.php
    

    index.php

    <!--
    File .......... index.php
    Author ........ Steve Haywood
    Website ....... http://www.spacewire.co.uk
    Project ....... SpaceWire UK Tutorial
    Version ....... 1.1
    Conception .... 8 August 2023
    Standard ...... PHP 7
    Description ...
      Homepage for Zedboard Webserver.
    -->
    
    <?php
    // Get information
    $sys_host = exec('hostname', $retval);
    $sys_time = exec('date', $retval);
    $sys_load = exec('awk \'{print $1}\' /proc/loadavg', $retval);
    $sys_up = exec('awk \'{print $1}\' /proc/uptime', $retval);
    $cpu_model = exec('grep model /proc/cpuinfo | cut -d : -f2 | tail -1 | sed \'s/\s//\'', $retval);
    $cpu_cores = exec('grep -c ^processor /proc/cpuinfo', $retval);
    $mem_total = exec('free -m | awk \'NR==2{print $2}\'', $retval);
    $mem_used = exec('free -m | awk \'NR==2{print $3}\'', $retval);
    $mem_free = exec('free -m | awk \'NR==2{print $4}\'', $retval);
    $net_mac = exec('cat /sys/class/net/eth0/address', $retval);
    $net_ip_loc = exec('ip a | grep inet | grep -vw lo | grep -v inet6 | cut -d \/ -f1 | sed \'s/[^0-9\.]*//g\'', $retval);
    $net_ip_ext = exec('wget -q -O- http://ipecho.net/plain', $retval);
    ?>
    
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <link href="../styles.css" rel="stylesheet">
    <title>Zedboard Webserver</title>
    </head>
    <body onload="add_register()">
    
    <div class="section"><h2>Zedboard Webserver</h2></div>
    
    <div class="section">
    <table>
    <thead>
    <tr><th colspan="3">Operating System Information <button onclick="read_os_ids()">Read ID</button></th>
    </tr>
    </thead>
    <tbody>
    <tr>
    <td style="text-align:right">Description :</td>
    <td style="text-align:left" id="oid_0">Unknown</td>
    <td><img id="osid_0" style="vertical-align:middle" src="../amber.gif" title="Unknown!" alt="Missing Image!"></td>
    </tr>
    <tr>
    <td style="text-align:right">Company :</td>
    <td style="text-align:left" id="oid_1">Unknown</td>
    <td><img id="osid_1" style="vertical-align:middle" src="../amber.gif" title="Unknown!" alt="Missing Image!"></td>
    </tr>
    <tr>
    <td style="text-align:right">Author :</td>
    <td style="text-align:left" id="oid_2">Unknown</td>
    <td><img id="osid_2" style="vertical-align:middle" src="../amber.gif" title="Unknown!" alt="Missing Image!"></td>
    </tr>
    <tr>
    <td style="text-align:right">Version :</td>
    <td style="text-align:left" id="oid_3">Unknown</td>
    <td><img id="osid_3" style="vertical-align:middle" src="../amber.gif" title="Unknown!" alt="Missing Image!"></td>
    </tr>
    <tr>
    <td style="text-align:right">Timestamp :</td>
    <td style="text-align:left" id="oid_4">Unknown</td>
    <td><img id="osid_4" style="vertical-align:middle" src="../amber.gif" title="Unknown!" alt="Missing Image!"></td>
    </tr>
    <tr>
    <td style="text-align:right">Hash :</td>
    <td style="text-align:left" id="oid_5">Unknown</td>
    <td><img id="osid_5" style="vertical-align:middle" src="../amber.gif" title="Unknown!" alt="Missing Image!"></td>
    </tr>
    </tbody>
    </table>
    <table>
    <thead>
    <tr><th colspan="3">Firmware Information <input type="submit" value="Read ID" id="read_ids" onclick="read_ids()"></th>
    </tr>
    </thead>
    <tbody>
    <tr>
    <td style="text-align:right">Description :</td>
    <td style="text-align:left" id="id_0">Unknown</td>
    <td><img id="sid_0" style="vertical-align:middle" src="../amber.gif" title="Unknown!" alt="Missing Image!"></td>
    </tr>
    <tr>
    <td style="text-align:right">Company :</td>
    <td style="text-align:left" id="id_1">Unknown</td>
    <td><img id="sid_1" style="vertical-align:middle" src="../amber.gif" title="Unknown!" alt="Missing Image!"></td>
    </tr>
    <tr>
    <td style="text-align:right">Author :</td>
    <td style="text-align:left" id="id_2">Unknown</td>
    <td><img id="sid_2" style="vertical-align:middle" src="../amber.gif" title="Unknown!" alt="Missing Image!"></td>
    </tr>
    <tr>
    <td style="text-align:right">Version :</td>
    <td style="text-align:left" id="id_3">Unknown</td>
    <td><img id="sid_3" style="vertical-align:middle" src="../amber.gif" title="Unknown!" alt="Missing Image!"></td>
    </tr>
    <tr>
    <td style="text-align:right">Timestamp :</td>
    <td style="text-align:left" id="id_4">Unknown</td>
    <td><img id="sid_4" style="vertical-align:middle" src="../amber.gif" title="Unknown!" alt="Missing Image!"></td>
    </tr>
    <tr>
    <td style="text-align:right">Hash :</td>
    <td style="text-align:left" id="id_5">Unknown</td>
    <td><img id="sid_5" style="vertical-align:middle" src="../amber.gif" title="Unknown!" alt="Missing Image!"></td>
    </tr>
    </tbody>
    </table>
    <table>
    <thead>
    <tr><th colspan="3">Loadable Firmware</th></tr>
    </thead>
    <tbody>
    <?php
      $dirPath = "/media/sd-mmcblk0p1/firmware";
      $files = scandir($dirPath);
      foreach ($files as $file) {
        $filePath = $dirPath . '/' . $file;
        if (is_file($filePath)) {
          echo "<tr><td style=\"text-align:left\">";
          echo $file;
          echo "</td><td><button onclick=\"loadfirmware('$file')\">Load</button></td><td><img id=\"fm_$file\" style=\"vertical-align:middle\" src=\"../amber.gif\" title=\"Unknown!\" alt=\"Missing Image!\"></td></tr>";
        }
      }
    ?>
    </tbody>
    </table>
    </div>
    
    <div class="section"><img src="../zedboard.png" alt="Missing Image!"></div>
    
    <div class="section">
    
    <table>
    <tr><th colspan="2">System</th></tr>
    <tr><td>Hostname</td>
    <td><?php echo $sys_host ?></td>
    </tr><tr><td>Time</td><td><?php echo $sys_time ?></td></tr>
    <tr><td>Uptime</td><td><span id="uptime_text"><?php echo $sys_up ?></span> seconds <button onclick="get_uptime()">Refresh</button> Auto :
    <select id="uptime" onchange="uptime();">
      <option value="0">Off</option>
      <option value="1">1s</option>
      <option value="5">5s</option>
    </select>
    </td></tr>
    </table>
    
    <table>
    <tr><th colspan="2">CPU</th></tr>
    <tr><td>Model</td>
    <td><?php echo $cpu_model ?></td></tr>
    <tr><td>Cores</td><td><?php echo $cpu_cores ?></td></tr>
    <tr><td>Load</td><td><?php echo $sys_load ?></td></tr>
    </table>
    
    <table>
    <tr><th colspan="2">Memory</th></tr>
    <tr><td>Total</td><td><?php echo $mem_total ?> Mb</td></tr>
    <tr><td>Used</td><td><?php echo $mem_used ?> Mb</td></tr>
    <tr><td>Free</td><td><?php echo $mem_free ?> Mb</td></tr>
    </table>
    
    <table>
    <tr><th colspan="2">Network</th></tr>
    <tr><td>MAC Address</td><td><?php echo $net_mac ?></td></tr>
    <tr><td>Internal IP</td><td><?php echo $net_ip_loc ?></td></tr>
    <tr><td>External IP</td><td><?php echo $net_ip_ext ?></td></tr>
    </table>
    
    </div>
    
    <div class="section">
    <table id="registers">
    <tr>
    <th>Address</th>
    <th>Hex</th>
    <th>Peek Value</th>
    <th>Sel</th>
    <th>Peek</th>
    <th>Status</th>
    <th>Copy</th>
    <th colspan="2">Poke Value</th>
    <th>Sel</th>
    <th>Poke</th>
    <th>Status</th>
    <th>Description</th>
    </tr>
    </table>
    <br><br>
    <input title="Add new row to end of address table" type="button" value="Add" onclick="add_row()">
    <select title="Set type of row to add to address table" id="type">
      <option value="0">Register</option>
      <option value="1">Section</option>
    </select>
    <input title="Remove last address from table" type="button" value="Remove" onclick="rem_register()">
    <input title="Peek all selected addresses in table" type="button" value="Peek All" onclick="peek_all()">
    <input title="Copy all table peek values into poke values" type="button" value="Copy All" onclick="copy_all()">
    <input title="Poke all selected addresses in table" type="button" value="Poke All" onclick="poke_all()">
    Peek Refresh :
    <select title="Set timer interval for automatic peek of table addresses" id="timer" onchange="timer()">
      <option value="0">Off</option>
      <option value="1">1s</option>
      <option value="5">5s</option>
    </select>
    Configuration :
    <button title="Create configuration file from table" onclick="create_config()">Create...</button> <a title="Right click and Save Link As... to locate and rename this file" download="config.txt" id="download" href="" style="display: none">config.txt</a>
    <input title="Read configuration file into table" type="file" id="load_config">
    </div>
    
    <div class="section">Designed by Steve Haywood @ 2021</div>
    
    <script src="../uptime.js"></script>
    
    </body>
    </html>
    
    Direct download available here :-
    steve@Desktop:~/projects/zedboard_linux$ wget https://spacewire.co.uk/tutorial/shared/repos/0025/zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/cgi-bin/index.php.txt -O os/petalinux/project-spec/meta-user/recipes-apps/website/files/cgi-bin/index.php
    
    Check out the changes.
    steve@Desktop:~/projects/zedboard_linux$ git difftool os/petalinux/project-spec/meta-user/recipes-apps/website/files/cgi-bin/index.php
    

    17. Update Javascript

    Update the Javascript to include support for the new features; range widget, select widget, individual hex/dec display & copy section.
    steve@Desktop:~/projects/zedboard_linux$ subl os/petalinux/project-spec/meta-user/recipes-apps/website/files/uptime.js
    

    uptime.js

    //
    // File .......... uptime.js
    // Author ........ Steve Haywood
    // Website ....... http://www.spacewire.co.uk
    // Project ....... SpaceWire UK Tutorial
    // Version ....... 1.6
    // Conception .... 17 January 2022
    // Standard ...... ECMA-262
    // Description ...
    //   Javascript functions for the dynamic webpage.
    //
    
    // Constants
    const c_uns = 0; // Unsigned string
    const c_hex = 1; // Hexadecimal string
    
    // Requests
    var timer_uptime;
    var timer_peek_all;
    
    // Load PL Firmware
    function loadfirmware(filename) {
      var url = "/cgi-bin/loadfirmware?" + filename;
      if (window.XMLHttpRequest) {
        var ajaxReq = new XMLHttpRequest();
        ajaxReq.onreadystatechange = function() {
          if (ajaxReq.readyState == 4 && ajaxReq.status == 200) {
            var respText = ajaxReq.responseText;
            var img_obj = document.getElementById("fm_" + filename);
            // Unique number is added to image to avoid caching issues on separate animations
            const now = Date.now();
            if (respText.substr(0,6) == "Error:") {
              img_obj.src = "../red.png?" + now;
              img_obj.title = "Last loadfirmware failed : " + respText.substr(7);
            } else {
              img_obj.src = "../green.gif?" + now;
              img_obj.title = "Last loadfirmware successful";
            }
          }
        }
        ajaxReq.open("POST", url, true);
        ajaxReq.send(null);
      }
    }
    
    // Download OS information file & display result
    async function read_os_ids() {
      let response = await fetch("/project.txt");
      if (response.status == 200) {
        let ids = await response.text();
        fields = ids.split(/\r?\n/);
      }
      for (var i = 0; i < 6; i++) {
        const now = Date.now();
        const txt_obj = document.getElementById("oid_" + i);
        const img_obj = document.getElementById("osid_" + i);
        if (response.status == 200) {
          if (i < fields.length && fields[i] != "") {
            img_obj.src = "../green.gif?" + now;
            img_obj.title = "Last file fetch successful";
            txt_obj.innerHTML = fields[i];
          } else {
            img_obj.src = "../red.gif?" + now;
            img_obj.title = "Missing field information";
            txt_obj.innerHTML = "Unknown";
          }
        } else {
          img_obj.src = "../red.gif?" + now;
          img_obj.title = "Last file fetch failed";
          txt_obj.innerHTML = "Unknown";
        }
      }
    }
    
    // Peek all strings
    function read_ids() {
      read_id(0x000, 0);
      read_id(0x080, 1);
      read_id(0x0C0, 2);
      read_id(0x100, 3);
      read_id(0x120, 4);
      read_id(0x140, 5);
    }
    
    // Peek string & display result
    function read_id(offset, reg) {
      var url = "/cgi-bin/peekstring?0x40000000&4096&" + offset + "&128";
      if (window.XMLHttpRequest) {
        var ajaxReq = new XMLHttpRequest();
        ajaxReq.onreadystatechange = function() {
          if (ajaxReq.readyState == 4 && ajaxReq.status == 200) {
            var respText = ajaxReq.responseText;
            var img_obj = document.getElementById("sid_" + reg);
            // Unique number is added to image to avoid caching issues on separate animations
            const now = Date.now();
            if (respText.substr(0,6) == "Error:") {
              img_obj.src = "../red.png?" + now;
              img_obj.title = "Last peekstring failed : " + respText.substr(7);
            } else {
              const now = Date.now();
              img_obj.src = "../green.gif?" + now;
              img_obj.title = "Last peekstring successful";
              document.getElementById("id_" + reg).innerHTML = respText;
            }
          }
        }
        ajaxReq.open("POST", url, true);
        ajaxReq.send(null);
      }
    }
    
    // Get uptime
    function get_uptime(reg) {
      var url = "/cgi-bin/uptime.cgi";
      if (window.XMLHttpRequest) {
        var ajaxReq = new XMLHttpRequest();
        ajaxReq.onreadystatechange = function() {
          if (ajaxReq.readyState == 4 && ajaxReq.status == 200) {
            var respText = ajaxReq.responseText;
            txtObj = document.getElementById("uptime_text");
            if (txtObj) {
              txtObj.innerHTML = respText;
            }
          }
        }
        ajaxReq.open("POST", url, true);
        ajaxReq.send(null);
      }
    }
    
    // Update uptime timer
    function uptime() {
      clearInterval(timer_uptime);
      var uptime = document.getElementById("uptime");
      var interval = uptime.value;
      if (interval > 0) {
        timer_uptime = setInterval("get_uptime()", 1000 * interval);
      }
    }
    
    // Update peek_all timer
    function timer() {
      clearInterval(timer_peek_all);
      var timer = document.getElementById("timer");
      interval = timer.value;
      if (interval > 0) {
        timer_peek_all = setInterval("peek_all()", 1000 * interval);
      }
    }
    
    // Convert unsigned/hexadecimal number string into a specified number string.
    function fmtUnsignedLong(string, format) {
      var hexStr;
      const value = parseInt(string);
      switch(format) {
        case c_uns:
          hexStr = value.toString(10);
          break;
        case c_hex:
          hexStr = value.toString(16).toUpperCase();
          hexStr = "0x" + "00000000".substr(0, 8 - hexStr.length) + hexStr;
          break;
        default:
          break;
      }
      return hexStr;
    }
    
    // Call fmtUnsignedLong with type setting from row in peek/poke address table.
    function fmtUnsignedLongReg(reg, value) {
      const disp_obj = document.getElementById("display_" + reg);
      var hexStr = "0xNaN";
      if (disp_obj) {
        const format = (disp_obj.checked) ? (c_hex) : (c_uns);
        hexStr = fmtUnsignedLong(value, format);
      }
      return hexStr;
    }
    
    
    // Copy peek to poke & update any associated widgets
    function copy(reg) {
      const peek_obj = document.getElementById("peek_" + reg);
      if (peek_obj) {
        const poke_obj = document.getElementById("poke_" + reg);
        if (poke_obj) {
          poke_obj.value = peek_obj.value;
        }
        const range_obj = document.getElementById("poke_range_" + reg);
        if (range_obj) {
          range_obj.value = parseInt(peek_obj.value);
        }
        const select_obj = document.getElementById("poke_select_" + reg);
        if (select_obj) {
          select_obj.value = peek_obj.value;
        }
      }
    }
    
    // Copy selected peek to poke in section
    function copy_section(row) {
      var obj_sel;
      do {
        row++;
        obj_sel = document.getElementById("peek_sel_" + row);
        if (obj_sel) {
          if (obj_sel.checked) {
            copy(row);
          }
        }
      } while (obj_sel);
    }
    
    // Copy all peek to poke
    function copy_all() {
      var table = document.getElementById("registers");
      var rows = table.rows.length - 1;
      for (var index = 0; index < rows; index++) {
        copy(index);
      }
    }
    
    // Peek address & display result
    function peek(reg) {
      var url = "/cgi-bin/peek?" + document.getElementById("addr_" + reg).value;
      if (window.XMLHttpRequest) {
        var ajaxReq = new XMLHttpRequest();
        ajaxReq.onreadystatechange = function() {
          if (ajaxReq.readyState == 4 && ajaxReq.status == 200) {
            var respText = ajaxReq.responseText;
            var img_obj = document.getElementById("speek_" + reg);
            // Unique number is added to image to avoid caching issues on separate animations
            const now = Date.now();
            if (respText.substr(0,6) == "Error:") {
              img_obj.src = "../red.gif?" + now;
              img_obj.title = "Last peek failed : " + respText.substr(7);
            } else {
              img_obj.src = "../green.gif?" + now;
              img_obj.title = "Last peek successful";
              document.getElementById("peek_" + reg).value = fmtUnsignedLongReg(reg, respText);
            }
          }
        }
        ajaxReq.open("POST", url, true);
        ajaxReq.send(null);
      }
    }
    
    // Peek all selected addresses in section
    function peek_section(row) {
      var obj_sel;
      do {
        row++;
        obj_sel = document.getElementById("peek_sel_" + row);
        if (obj_sel) {
          if (obj_sel.checked) {
            peek(row);
          }
        }
      } while (obj_sel);
    }
    
    // Peek all selected addresses in table
    function peek_all() {
      var table = document.getElementById("registers");
      var rows = table.rows.length - 1;
      for (var index = 0; index < rows; index++) {
        const obj_sel = document.getElementById("peek_sel_" + index);
        if (obj_sel) {
          if (obj_sel.checked) {
            peek(index);
          }
        }
      }
    }
    
    // Poke address & display result
    function poke(reg) {
      var url = "/cgi-bin/poke?" + document.getElementById("addr_" + reg).value + "&" + document.getElementById("poke_" + reg).value;
      if (window.XMLHttpRequest) {
        var ajaxReq = new XMLHttpRequest();
        ajaxReq.onreadystatechange = function() {
          if (ajaxReq.readyState == 4 && ajaxReq.status == 200) {
            var respText = ajaxReq.responseText;
            var img_obj = document.getElementById("spoke_" + reg);
            // Unique number is added to image to avoid caching issues on separate animations
            const now = Date.now();
            if (respText.substr(0,6) == "Error:") {
              img_obj.src = "../red.gif?" + now;
              img_obj.title = "Last poke failed : " + respText.substr(7);
            } else {
              img_obj.src = "../green.gif?" + now;
              img_obj.title = "Last poke successful";
            }
          }
        }
        ajaxReq.open("POST", url, true);
        ajaxReq.send(null);
      }
    }
    
    // Poke value from input widget & update any associated widgets
    function poke_widget(reg) {
      const poke_obj = document.getElementById("poke_" + reg);
      if (poke_obj) {
        const range_obj = document.getElementById("poke_range_" + reg);
        if (range_obj) {
          range_obj.value = parseInt(poke_obj.value);
        }
        const select_obj = document.getElementById("poke_select_" + reg);
        if (select_obj) {
          select_obj.value = poke_obj.value;
        }
      }
      poke(reg);
    }
    
    // Poke value on Enter key from within input widget
    // Holding down Enter keeps on poking which is not desired - needs improvement!
    function poke_key(reg) {
      if (event.key === 'Enter') {
        poke_widget(reg);
      }
    }
    
    // Update radix for peek & poke input widgets
    function update_radix(reg) {
      const poke_obj = document.getElementById("poke_" + reg);
      if (poke_obj) {
        poke_obj.value = fmtUnsignedLongReg(reg, poke_obj.value);
      }
      const peek_obj = document.getElementById("peek_" + reg);
      if (peek_obj) {
        peek_obj.value = fmtUnsignedLongReg(reg, peek_obj.value);
      }
    }
    
    // Poke value from range widget & update any associated widgets
    function poke_range(reg) {
      const range_obj = document.getElementById("poke_range_" + reg);
      if (range_obj) {
        const poke_obj = document.getElementById("poke_" + reg);
        if (poke_obj) {
          poke_obj.value = fmtUnsignedLongReg(reg, range_obj.value);
        }
      }
      poke(reg);
    }
    
    // Poke value from select widget & update any associated widgets
    function poke_select(reg) {
      const select_obj = document.getElementById("poke_select_" + reg);
      if (select_obj) {
        const poke_obj = document.getElementById("poke_" + reg);
        if (poke_obj) {
          poke_obj.value = fmtUnsignedLongReg(reg, select_obj.value);
        }
      }
      poke(reg);
    }
    
    // Poke all selected addresses in section
    function poke_section(row) {
      var obj_sel;
      do {
        row++;
        obj_sel = document.getElementById("poke_sel_" + row);
        if (obj_sel) {
          if (obj_sel.checked) {
            poke_widget(row);
          }
        }
      } while (obj_sel);
    }
    
    // Poke all selected addresses in table
    function poke_all() {
      var table = document.getElementById("registers");
      var rows = table.rows.length - 1;
      for (var index = 0; index < rows; index++) {
        const obj_sel = document.getElementById("poke_sel_" + index);
        if (obj_sel) {
          if (obj_sel.checked) {
            poke_widget(index);
          }
        }
      }
    }
    
    // Add row to table
    function add_row() {
      const obj_type = document.getElementById("type");
      const row_type = obj_type.value;
      switch(row_type) {
        case "0":
          add_register();
          break;
        case "1":
          add_section();
          break;
        default:
          break;
      }
    }
    
    // Add peek/poke row to table
    function add_register(reg) {
      const table = document.getElementById("registers");
      const next = table.rows.length - 1;
      var addr = 0x40010000;
      if (next > 0) {
        const obj_addr = document.getElementById("addr_" + (next - 1));
        if (obj_addr) {
          addr = parseInt(obj_addr.value) + 4;
        }
      }
      const fields = ["reg", addr.toString(), "true", "true", "0x00000000", "true", "Register @ " + fmtUnsignedLong(addr.toString(), c_hex)];
      add_row_raw(fields);
    }
    
    // Add section row to table
    function add_section(reg) {
      const fields = ["sec", "Section Description"];
      add_row_raw(fields);
    }
    
    // Add row to table
    function add_row_raw(fields) {
      const table = document.getElementById("registers");
      const next = table.rows.length - 1;
      const row = table.insertRow(-1);
      var newcell;
    
      // Separate out fields
      var type            = fields[0];
      if (type == "sec") {
        var description   = fields[1];
      } else {
        var address       = fields[1];
        var display_type  = fields[2];
        var peek_select   = fields[3];
        var options       = fields[4].split("#");
        var poke_select   = fields[5];
        var description   = fields[6];
        // Decode fields
        var disp_checked  = (display_type == "true") ? ("checked") : ("");
        var format        = (display_type == "true") ? (c_hex) : (c_uns);
        var peek_checked  = (peek_select == "true") ? ("checked") : ("");
        var poke_checked  = (poke_select == "true") ? ("checked") : ("");
      }
    
      newcell = row.insertCell(-1);
      if (type == "sec") {
        newcell.colSpan = "4";
        newcell.innerHTML = "--- Section ---";
      } else {
        newcell.innerHTML = '<input title="Address to peek/poke" type="text" id="addr_' + next + '" value="' + fmtUnsignedLong(address, c_hex) + '" size="10">';
    
        newcell = row.insertCell(-1);
        newcell.innerHTML = '<input title="Select peak/poke display type (unsigned/hexadecimal)" type="checkbox" id="display_' + next + '" ' + disp_checked + ' onchange="update_radix(' + next + ')">';
    
        newcell = row.insertCell(-1);
        newcell.innerHTML = '<input title="Value peeked at address" type="text" id="peek_' + next + '" value="'+ fmtUnsignedLong("0x0", format) +'" size="10" readonly="readonly">';
    
        newcell = row.insertCell(-1);
        newcell.innerHTML = '<input title="Select address for peeking" type="checkbox" id="peek_sel_' + next + '" ' + peek_checked + '>';
      }
    
      newcell = row.insertCell(-1);
      if (type == "sec") {
        newcell.innerHTML = '<input title="Peek all selected addresses in section" type="submit" value="Peek" onclick="peek_section(' + next + ')">';
      } else {
        newcell.innerHTML = '<input title="Peek address" type="submit" value="Peek" onclick="peek(' + next + ')">';
      }
    
      newcell = row.insertCell(-1);
      if (type == "sec") {
        newcell.innerHTML = '<img style="vertical-align:middle" src="../amber.gif" alt="Missing Image!">';
      } else {
        newcell.innerHTML = '<img title="Peek status" id="speek_' + next + '" style="vertical-align:middle" src="../amber.gif" alt="Missing Image!">';
      }
    
      newcell = row.insertCell(-1);
      if (type == "sec") {
        newcell.innerHTML = '<input title="Copy all selected peek values into poke values in section" type="submit" value=">>" onclick="copy_section(' + next + ')">';
      } else {
        newcell.innerHTML = '<input title="Copy peek value into poke value" type="submit" value=">>" onclick="copy(' + next + ')">';
      }
    
      newcell = row.insertCell(-1);
      switch (type) {
        case "sec":
          newcell.colSpan = "3";
          newcell.innerHTML = "--- Section ---";
          break;
        case "reg":
          const poke_value    = fields[4];
          newcell.innerHTML = '<input title="Value to poke at address" type="text" id="poke_' + next + '" value="'+ fmtUnsignedLong(poke_value, format) +'" size="10" onkeydown="poke_key(' + next + ')">';
          newcell = row.insertCell(-1);
          newcell.style.padding = "0";
          break;
        case "range":
          var range_value   = options[0];
          var range_min     = options[1];
          var range_max     = options[2];
          newcell.innerHTML = '<input title="Value to poke at address" type="text" id="poke_' + next + '" value="'+ fmtUnsignedLong(range_value, format) +'" size="10" onkeydown="poke_key(' + next + ')">';
          newcell = row.insertCell(-1);
          newcell.innerHTML = '<input title="Value to poke at address" style="width:100%" type="range" min="'+ range_min +'" max="'+ range_max +'" value="'+ range_value +'" id="poke_range_' + next + '" oninput="poke_range(' + next + ')">';
          break;
        case "select":
          var opt_str = "";
          var opt_sel;
          var opt_pair;
          const sel = options[0];
          for (var index = 1; index < options.length; index++) {
            opt_pair = options[index].split("^");
            opt_sel = (sel == index - 1) ? (" selected") : ("");
            opt_str = opt_str.concat('<option value="', opt_pair[0], '"', opt_sel + '>', opt_pair[1], '</option>');
          }
          newcell.innerHTML = '<input title="Value to poke at address" type="text" id="poke_' + next + '" value="'+ fmtUnsignedLong(sel, format) +'" size="10" onkeydown="poke_key(this, ' + next + ')">';
          newcell = row.insertCell(-1);
          newcell.innerHTML = '<select title="Value to poke at address" style="width:100%" id="poke_select_' + next + '" onchange="poke_select(' + next + ')">' + opt_str + '</select>';
          break;
        default:
          break;
      }
    
      if (type == "sec") {
        newcell = row.insertCell(-1);
        newcell.innerHTML = '<input title="Poke all selected addresses in section" type="submit" value="Poke" onclick="poke_section(' + next + ')">';
    
        newcell = row.insertCell(-1);
        newcell.innerHTML = '<img style="vertical-align:middle" src="../amber.gif" alt="Missing Image!">';
    
        newcell = row.insertCell(-1);
        newcell.innerHTML = '<input title="Description of section" type="text" id="name_' + next + '" value="' + description + '" size="40">';
      } else {
        newcell = row.insertCell(-1);
        newcell.innerHTML = '<input title="Select address for poking" type="checkbox" id="poke_sel_' + next + '" ' + poke_checked + '>';
    
        newcell = row.insertCell(-1);
        newcell.innerHTML = '<input title="Poke address" type="submit" value="Poke" onclick="poke_widget(' + next + ')">';
    
        newcell = row.insertCell(-1);
        newcell.innerHTML = '<img title="Poke status" id="spoke_' + next + '" style="vertical-align:middle" src="../amber.gif" alt="Missing Image!">';
    
        newcell = row.insertCell(-1);
        newcell.innerHTML = '<input title="Description of address" type="text" id="name_' + next + '" value="' + description + '" size="40">';
      }
    }
    
    // Remove row from table
    function rem_register(reg) {
      var table = document.getElementById("registers");
      if (table.rows.length > 1) {
        table.deleteRow(-1);
      }
    }
    
    // Remove all rows from table
    function remove_all() {
      var table = document.getElementById("registers");
      var rows = table.rows.length - 1;
      for (var index = 0; index < rows; index++) {
        table.deleteRow(-1);
      }
    }
    
    // Note: browser file access is made difficult for security reasons - there maybe a better way of doing file read & write.
    
    var config_file = null;
    
    // Create virtual configuration file from address table for user to download
    function create_config() {
      var text = "";
      var table = document.getElementById("registers");
      const rows = table.rows.length - 1;
      for (var index = 0; index < rows; index++) {
        const obj_addr = document.getElementById("addr_" + index);
        if (obj_addr) { // Register type
          var addr = document.getElementById("addr_" + index).value;
          var display = document.getElementById("display_" + index).checked;
          var peek_sel = document.getElementById("peek_sel_" + index).checked;
          var poke = document.getElementById("poke_" + index).value;
          var poke_sel = document.getElementById("poke_sel_" + index).checked;
          var name = document.getElementById("name_" + index).value;
          text += "reg" + "|" + addr + "|" + display + "|" + peek_sel + "|" + poke + "|" + poke_sel + "|"+ name + "\n";
        } else { // Section type
          var name = document.getElementById("name_" + index).value;
          text += "sec" + "|" + name + "\n";
        }
      }
      const data = new Blob([text], {type: 'text/plain'});
      if (config_file !== null) {
        URL.revokeObjectURL(config_file);
      }
      config_file = URL.createObjectURL(data);
      var link = document.getElementById('download');
      link.href = config_file;
      link.style.display = 'inline';
    }
    
    // Read configuration file and update address table
    function load_config(input) {
      var file = input.target.files[0];
      if (file) {
        var reader = new FileReader();
        reader.onload = function(input) {
          var contents = input.target.result;
          const lines = contents.split(/\r\n|\n/);
          remove_all();
          lines.forEach((line) => {
            if (line.length > 0) {
              var table = document.getElementById("registers");
              var next = table.rows.length - 1;
              const values = line.split("|");
              switch(values[0]) {
                case "reg":
                case "sec":
                case "range":
                case "select":
                  add_row_raw(values);
                  break;
                default:
                  alert("Error: Unrecognized table type found (" + values[0] + "), ignoring.");
              }
            }
          });
        };
        reader.readAsText(file);
      }
    }
    
    document.getElementById('load_config').addEventListener('change', load_config, false);
    
    Direct download available here :-
    steve@Desktop:~/projects/zedboard_linux$ wget https://spacewire.co.uk/tutorial/shared/repos/0025/zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/uptime.js -O os/petalinux/project-spec/meta-user/recipes-apps/website/files/uptime.js
    
    Check out the changes.
    steve@Desktop:~/projects/zedboard_linux$ git difftool os/petalinux/project-spec/meta-user/recipes-apps/website/files/uptime.js
    

    18. Check everything is working as expected

    Copy the updated HTML & Javascript files over to PetaLinux.
    steve@Desktop:~/projects/zedboard_linux$ ssh-keygen -f /home/steve/.ssh/known_hosts -R 192.168.2.87
    steve@Desktop:~/projects/zedboard_linux$ ssh-keyscan -H 192.168.2.87 >> ~/.ssh/known_hosts
    steve@Desktop:~/projects/zedboard_linux$ sshpass -p root scp os/petalinux/project-spec/meta-user/recipes-apps/website/files/cgi-bin/index.php root@192.168.2.87:/srv/www/cgi-bin
    steve@Desktop:~/projects/zedboard_linux$ sshpass -p root scp os/petalinux/project-spec/meta-user/recipes-apps/website/files/uptime.js root@192.168.2.87:/srv/www
    
    Access the webserver running on the Zedboard using a browser pointing at the Zedboard's IP address (192.168.2.87/cgi-bin/index.php). All being well something akin to the following webpage should be displayed. Missing Image! Update the LEDs & Switches configuration file to include the newly created range & select HTML widgets.

    Note the changes :-
    steve@Desktop:~/projects/zedboard_linux$ subl os/src/other/zedboard_leds_switches.txt
    

    zedboard_leds_switches.txt

    sec|AXI General Purpose IO - Zedboard Specific
    range|0x40010000|false|true|0#0#255|true|LEDs
    reg|0x40010008|true|true|0x00000000|false|DIP Switches
    reg|0x40010010|true|true|0x00000000|false|Push Buttons
    select|0x4001011C|true|true|0#0x00000000^Disabled#0x80000000^Enabled|true|Interrupt Enable
    select|0x40010128|false|true|0#0^No Channel#1^Channel 1 (LEDs)#2^Channel 2 (DIP Switches)#4^Channel 3 (Push Buttons)|true|Interrupt Enable
    select|0x40010120|false|true|0#0^No Channel#1^Channel 1 (LEDs)#2^Channel 2 (DIP Switches)#4^Channel 3 (Push Buttons)|true|Interrupt Status
    
    sec|AXI Register Bank
    reg|0x40020000|true|true|0x00000000|true|Register 0
    reg|0x40020004|true|true|0x456789AB|true|Register 1
    reg|0x40020008|true|true|0x00000000|true|Register 2
    reg|0x4002000C|true|true|0x0000FF00|true|Register 3
    
    Direct download available here :-
    steve@Desktop:~/projects/zedboard_linux$ wget https://spacewire.co.uk/tutorial/shared/repos/0025/zedboard_linux/os/src/other/zedboard_leds_switches.txt -O os/src/other/zedboard_leds_switches.txt
    
    Use the Browse button on the Webpage to select the updated zedboard_leds_switches.txt from /home/steve/os/src/others. All being well a more user friendly peek/poke address interface should be displayed.

    The video below shows the following sequence of evens.
    1. LEDs counting up from 0 (none illuminated) to 255 (all illuminated).
    2. DIP switches 0 & 7 changing from off to on.
    3. Interrupts being enabled for channel 3 (push buttons).
    4. Centre push button being pressed to trigger an interrupt.
    5. Triggered interrupt and button state being cleared.
    6. Interrupts being disabled.
    7. Left push button being pressed (no interrupt this time).
    #### Part 5 - Revisions Control ####

    19. Commit new & updated files

    Remove legacy configuration file & add new load firmware source file.
    steve@Desktop:~/projects/zedboard_linux$ git rm os/src/other/axi_gpio_zed.txt
    steve@Desktop:~/projects/zedboard_linux$ git add os/petalinux/project-spec/meta-user/recipes-apps/peekpokecgi/files/loadfirmware.c
    
    Check GIT status to make sure all is well and there are no spurious elements.
    steve@Desktop:~/projects/zedboard_linux$ git status
    On branch master
    Your branch is up-to-date with 'origin/master'.
    
    Changes to be committed:
      (use "git restore --staged <file>..." to unstage)
            new file:   os/petalinux/project-spec/meta-user/recipes-apps/peekpokecgi/files/loadfirmware.c      
            deleted:    os/src/other/axi_gpio_zed.txt      
    
    Changes not staged for commit:
      (use "git add <file>..." to update what will be committed)
      (use "git restore <file>..." to discard changes in working directory)
            modified:   os/petalinux/project-spec/meta-user/recipes-apps/peekpokecgi/files/Makefile      
            modified:   os/petalinux/project-spec/meta-user/recipes-apps/peekpokecgi/peekpokecgi.bb      
            modified:   os/petalinux/project-spec/meta-user/recipes-apps/website/files/cgi-bin/index.php      
            modified:   os/petalinux/project-spec/meta-user/recipes-apps/website/files/project.txt      
            modified:   os/petalinux/project-spec/meta-user/recipes-apps/website/files/uptime.js      
            modified:   os/src/other/zedboard_leds_switches.txt      
    
    Looks good!

    Commit the updates, create an annotated tag and push the commit & tag up to the remote repository.
    steve@Desktop:~/projects/zedboard_linux$ git commit -a -m "Added the ability to load new PL firmware without a rebuild or reboot of PetaLinux. Updated the peek/poke address table to include range & select HTML elements."
    steve@Desktop:~/projects/zedboard_linux$ git push
    steve@Desktop:~/projects/zedboard_linux$ git tag -a v13.0 -m "PetaLinux, Peek/Poke, LED Runner, LAMP (Apache, SQLite, PHP & myLiteAdmin), Peek/Poke CGI, Load Firmware CGI, PL Access, Style Sheet, Register Bank & ID Strings with XSA from zedboard_leds_switches v5.0"
    steve@Desktop:~/projects/zedboard_linux$ git push origin v13.0
    
    steve@Desktop:~/projects/zedboard_linux$ cd os/petalinux
    
    #### Part 6 - Final checks ####

    20. Create a Production Release

    With the project area assumed clean and everything committed & pushed into the repository, a potential production release of PetaLinux (v13.0) can be produced.

    Double check GIT status.
    steve@Desktop:~/projects/zedboard_linux/os/petalinux$ git status
    On branch master
    Your branch is up-to-date with 'origin/master'.
    
    nothing to commit, working tree clean
    
    Optionally, but preferred, clear out all the superfluous files from the project area (non-tracked files).
    steve@Desktop:~/projects/zedboard_linux/os/petalinux$ git clean -fdx
    
    Optionally, but preferred, clear out the transfer area.
    steve@Desktop:~/projects/zedboard_linux/os/petalinux$ rm -rf /tftpboot/*
    
    Build/rebuild PetaLinux.
    steve@Desktop:~/projects/zedboard_linux/os/petalinux$ petalinux-build
    
    Package PetaLinux to produce the boot image BOOT.BIN, this will include the first stage boot loader zynq_fsbl.elf, the programmable logic system.bit, the Linux boot loader u-boot.elf and the device tree blob system.dtb.
    steve@Desktop:~/projects/zedboard_linux/os/petalinux$ petalinux-package --boot --force --fsbl images/linux/zynq_fsbl.elf --fpga images/linux/system.bit --uboot images/linux/u-boot.elf
    

    21. Deploy Production Release on the Zedboard

    Check the Zedboard is present on the network & PetaLinux is up and running.
    steve@Desktop:~/projects/zedboard_linux/os/petalinux$ ping -c 1 192.168.2.87
    
    Enable a pathway to PetaLinux running on the Zedboard.
    steve@Desktop:~/projects/zedboard_linux/os/petalinux$ ssh-keygen -f ~/.ssh/known_hosts -R 192.168.2.87
    steve@Desktop:~/projects/zedboard_linux/os/petalinux$ ssh-keyscan -H 192.168.2.87 >> ~/.ssh/known_hosts
    
    Upload the new PetaLinux boot files to the Zedboard.
    steve@Desktop:~/projects/zedboard_linux/os/petalinux$ sshpass -p root scp /tftpboot/{BOOT.BIN,boot.scr,image.ub} root@192.168.2.87:/media/sd-mmcblk0p1
    
    Reboot PetaLinux running on the Zedboard to make use of the new boot files.
    steve@Desktop:~/projects/zedboard_linux/os/petalinux$ sshpass -p root ssh -t root@192.168.2.87 /sbin/reboot
    

    22. Check everything is working as expected

    Access the webserver running on the Zedboard using a browser pointing at the Zedboard's IP address (192.168.2.87/cgi-bin/index.php). Click the Read ID buttons in both the Operating System Information & Firmware Information sectons to read the Identification information from the OS filesystem & PL address space. All being well the following should be displayed in the Operating System Information & Firmware Information sections.

    There should be no unsavory comments after the version numbers!