Bar
SpaceWire UK
Specialist providers of VHDL Intellectual Property & Design Services
BarBarBarBar
Tutorial
Missing Image!
Part 20 - Add bank of registers inside PL and access via Webserver (28 November 2021)

Introduction

This tutorial details the steps required to add a general purpose bank of registers inside the Programmable Logic part of the FPGA. Control of these registers shall be via the Webserver, which will be enhanced to allow access to more than just one register. The peek & CGI access will be updated to use the binaries instead of the scripts that call the binaries. Note Part 17 of this tutorial is newly added and is a prerequisite for this part.

Aims

The aims of this tutorial are as follows :-
  1. Setup environment
  2. Change present working directory
  3. Launch Vivado & open project
  4. Create AXI4-Lite peripheral
  5. Open block design
  6. Re-customize AXI GPIO
  7. Add peripheral to block design
  8. Edit address map
  9. Generate bitstream
  10. Export hardware platform
  11. Setup Zedboard hardware
  12. Deploy PetaLinux on Zedboard
  13. Program PL independently
  14. Check PL is working as expected
  15. Enhance website peek & poke access
  16. Update Javascript for enhanced website
  17. Quick check enhanced website is working as expected
  18. Rebuild PetaLinux
  19. Deploy PetaLinux on Zedboard
  20. Check everything is working as expected
  21. Files for revision control

1. Setup environment

Setup Xilinx design environment for the 2021.2 toolset.
steve@Linux-Steve:/home/steve$ source xilinx.sh
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. Change present working directory

Change the present working directory to be the project directory.
steve@Linux-Steve:/home/steve$ cd /home/steve/projects/leds_switches/fw

3. Launch Vivado & open project

Launch Vivado quietly from a Terminal.
steve@Linux-Steve:/home/steve/projects/leds_switches/fw$ vivado -nojournal -nolog -notrace project.xpr &

4. Create AXI4-Lite peripheral

To create an AXI4-Lite peripheral (register bank) navigate Tools » Create and Package New IP....

In the Create and Package New IP dialog click Next to proceed. Missing Image! Select Create AXI4 Peripheral in the Create and Package New IP - Create Peripheral, Package IP or Package a Block Design dialog and click Next to proceed. Missing Image! Populate the following details in the Create and Package New IP - Peripheral Details dialog and click Next to proceed. Missing Image! In the Create and Package New IP - Add Interfaces dialog leave the defaults as shown and click Next to proceed. Missing Image! Review the information provided in the Create and Package New IP - Create Peripheral dialog and click Finish to proceed. Missing Image! Quite a few useful files have now been created in the ip_repo directory, including a testbench that uses the Xilinx AXI4 Verification IP. The two main files of interest are the HDL sources, which can be found in the hdl directory. Examine these to get a better understanding of what they are doing. The operational code is provided in register_bank_v1_0_S00_AXI.v along with a wrapper provided in register_bank_v1_0.v.
steve@Linux-Steve:/home/steve/projects/leds_switches/fw$ subl ../usr/ip_repo/register_bank_1.0/hdl/{register_bank_v1_0_S00_AXI.v,register_bank_v1_0.v}

5. Open block design

To open the existing block design click on Open Block Design under IP INTEGRATOR. Get a better view of the Block Design by clicking on its Float Missing Image! icon. Missing Image!

6. Re-customize AXI GPIO

Double click on the AXI GPIO module to begin re-customization. Change the Default Output Value of GPIO to 0x00000081 so the updated PL design is easily identifiable from the previous one. Click OK to commit the changes. Missing Image!

7. Add peripheral to block design

Add the newly created peripheral to the block design by clicking on Add IP Missing Image! and then selecting AXI Register Bank from the context menu. Missing Image! Connect the Register Bank to the rest of the system by clicking on Run Connection Automation.

In the Run Connection Automation dialog tick All Automation and click OK to proceed. Missing Image! Verify the block design is error free by clicking on the Validate Design Missing Image! icon. All being well the Validation successful dialog should now appear. Missing Image!

8. Edit address map

Check the address map assignment inside the Address Editor under the BLOCK DESIGN section. The register_bank_0 module should have a Master Base Address of 0x43C0_0000 , if not change it. Change the Range to 4K. Missing Image! Save the Block Design.

9. Generate bitstream

Generate the programmable logic bitstream by clicking on Generate Bitstream under the PROGRAM AND DEBUG heading inside the Flow Navigator section. Missing Image!

10. Export hardware platform

Export the hardware platform by selecting File » Export » Export Hardware... from the main menu. Include the bitstream in the exported hardware file and save as /home/steve/projects/leds_switches/fw/system_wrapper.xsa.

11. Setup Zedboard hardware

If not already, connect up the hardware as follows and power on the Zedboard :-
  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 JTAG. Missing Image!

12. Deploy PetaLinux on Zedboard

If not already, deploy the PetaLinux image via JTAG.
steve@Linux-Steve:/home/steve/projects/leds_switches/fw$ cd ../../petalinux
steve@Linux-Steve:/home/steve/projects/petalinux$ petalinux-boot --jtag --prebuilt 3

13. Program PL independently

The Programmable Logic part of the FPGA can be reloaded while Linux is running without requiring a Linux reboot. To achieve this safely the interfaces between the PL & PS must be inactive and not transferring data.

Program the PL from Vivado by clicking on Open Hardware Manager under PROGRAM AND DEBUG. Missing Image! In the HARDWARE MANAGER click on Open target and select Auto Connect from the context menu. Missing Image! In the HARDWARE MANAGER click on Program device. Missing Image! In the Program Device dialog click Program to program the PL. Missing Image!

14. Check PL is working as expected

All being well there should now exist four 32-bit registers within the address space 0x43C0_0000 to 0x43C0_000F.

Using the Client URL (curl) command enter the following URL's to command the peek & poke CGI binaries to read & write from & to the address space.
steve@Linux-Steve:/home/steve/projects/petalinux$ curl -w "\nReceived %{size_download} bytes\n" "http://192.168.2.87/cgi-bin/peek?0x43C00000"
0x00000000
Received 10 bytes
steve@Linux-Steve:/home/steve/projects/petalinux$ curl -w "\nReceived %{size_download} bytes\n" "http://192.168.2.87/cgi-bin/poke?0x43C00000&0x456789ab"
Success
Received 7 bytes
steve@Linux-Steve:/home/steve/projects/petalinux$ curl -w "\nReceived %{size_download} bytes\n" "http://192.168.2.87/cgi-bin/peek?0x43C00000"
0x456789AB
Received 10 bytes

15. Enhance website peek & poke access

Edit the existing webpage to enhance its peek & poke capabilities such that multiple registers can be accessed. Add in global controls for the multiple registers and also file access so the register configuration can be saved and re-loaded.
steve@Linux-Steve:/home/steve/projects/petalinux$ subl project-spec/meta-user/recipes-apps/website/files/cgi-bin/index.cgi

index.cgi

  1. #!/bin/sh

  2. # Output Header
  3. printf "Content-type: text/html\n\n"

  4. # Get information
  5. sys_host=$(hostname)
  6. sys_time=$(date)
  7. sys_load=$(awk '{print $1}' /proc/loadavg)
  8. sys_up=$(awk '{print $1}' /proc/uptime)
  9. cpu_model=$(grep model /proc/cpuinfo | cut -d : -f2 | tail -1 | sed 's/\s//')
  10. cpu_cores=$(grep -c ^processor /proc/cpuinfo)
  11. mem_total=$(free -m | awk 'NR==2{print $2}')
  12. mem_used=$(free -m | awk 'NR==2{print $3}')
  13. mem_free=$(free -m | awk 'NR==2{print $4}')
  14. net_mac=$(cat /sys/class/net/eth0/address)
  15. net_ip_loc=$(ip a | grep inet | grep -vw lo | grep -v inet6 | cut -d \/ -f1 | sed 's/[^0-9\.]*//g')
  16. net_ip_ext=$(wget -q -O- http://ipecho.net/plain)

  17. # Output HTML
  18. cat <<EOF
  19. <!DOCTYPE html>
  20. <html lang="en">
  21. <head>
  22. <meta http-equiv="content-type" content="text/html; charset=UTF-8">
  23. <link href="../styles.css" rel="stylesheet">
  24. <title>Zedboard Webserver</title>
  25. </head>
  26. <body onload="add_register()">

  27. <div class="section"><h2>Zedboard Webserver</h2></div>

  28. <div class="section"><img src="../zedboard.png" alt="Missing Image!"></div>

  29. <div class="section">

  30. <table>
  31. <tr><th colspan="2">System</th></tr>
  32. <tr><td>Hostname</td>
  33. <td>${sys_host}</td>
  34. </tr><tr><td>Time</td><td>${sys_time}</td></tr>
  35. <tr><td>Uptime</td><td><span id="uptime_text">${sys_up}</span> seconds <button onclick="get_uptime()">Refresh</button> Auto :
  36. <select id="uptime" onchange="uptime();">
  37.   <option value="0">Off</option>
  38.   <option value="1">1s</option>
  39.   <option value="5">5s</option>
  40. </select>
  41. </td></tr>
  42. </table>

  43. <table>
  44. <tr><th colspan="2">CPU</th></tr>
  45. <tr><td>Model</td>
  46. <td>${cpu_model}</td></tr>
  47. <tr><td>Cores</td><td>${cpu_cores}</td></tr>
  48. <tr><td>Load</td><td>${sys_load}</td></tr>
  49. </table>

  50. <table>
  51. <tr><th colspan="2">Memory</th></tr>
  52. <tr><td>Total</td><td>${mem_total} Mb</td></tr>
  53. <tr><td>Used</td><td>${mem_used} Mb</td></tr>
  54. <tr><td>Free</td><td>${mem_free} Mb</td></tr>
  55. </table>

  56. <table>
  57. <tr><th colspan="2">Network</th></tr>
  58. <tr><td>MAC Address</td><td>${net_mac}</td></tr>
  59. <tr><td>Internal IP</td><td>${net_ip_loc}</td></tr>
  60. <tr><td>External IP</td><td>${net_ip_ext}</td></tr>
  61. </table>

  62. </div>

  63. <div class="section">
  64. <table id="registers">
  65. <tr>
  66. <th>Address</th>
  67. <th>Peek Value</th>
  68. <th>Peek</th>
  69. <th>Status</th>
  70. <th>Copy</th>
  71. <th>Poke Value</th>
  72. <th>Poke</th>
  73. <th>Status</th>
  74. <th>Description</th>
  75. </tr>
  76. </table>
  77. <br><br>
  78. <input title="Add new address at end of table" type="button" value="Add" onclick="add_register()">
  79. <input title="Remove last address from table" type="button" value="Remove" onclick="rem_register()">
  80. <input title="Peek all addresses in table" type="button" value="Peek All" onclick="peek_all()">
  81. <input title="Copy all table peek values into poke values" type="button" value="Copy All" onclick="copy_all()">
  82. <input title="Poke all addresses in table" type="button" value="Poke All" onclick="poke_all()">
  83. Peek Refresh :
  84. <select title="Set timer interval for automatic peek of table addresses" id="timer" onchange="timer()">
  85.   <option value="0">Off</option>
  86.   <option value="1">1s</option>
  87.   <option value="5">5s</option>
  88. </select>
  89. Number Format :
  90. <select title="Set number format for peek and poke values" id="format" onchange="format()">
  91.   <option value="0">Hexadecimal</option>
  92.   <option value="1">Unsigned</option>
  93. </select>
  94. Configuration :
  95. <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>
  96. <input title="Read configuration file into table" type="file" id="load_config">
  97. </div>

  98. <div class="section">Designed by Steve Haywood @ 2021</div>

  99. <script src="../uptime.js"></script>

  100. </body>
  101. </html>
  102. EOF

16. Update Javascript for enhanced website

Edit the existing Javascript to add in the functions required to support the enhanced website. The Javascript at this point is updated to be more flexible for future use. Note the addition of file operations for configuration file access.
steve@Linux-Steve:/home/steve/projects/petalinux$ subl project-spec/meta-user/recipes-apps/website/files/uptime.js

uptime.js

  1. // Requests
  2. var timer_uptime;
  3. var timer_peek_all;

  4. // Get uptime
  5. function get_uptime(reg) {
  6.   var url = "cgi-bin/uptime.cgi";
  7.   if (window.XMLHttpRequest) {
  8.     var ajaxReq = new XMLHttpRequest();
  9.     ajaxReq.onreadystatechange = function() {
  10.       if (ajaxReq.readyState == 4 && ajaxReq.status == 200) {
  11.         var respText = ajaxReq.responseText;
  12.         txtObj = document.getElementById("uptime_text");
  13.         if (txtObj) {
  14.           txtObj.innerHTML = respText;
  15.         }
  16.       }
  17.     }
  18.     ajaxReq.open("POST", url, true);
  19.     ajaxReq.send(null);
  20.   }
  21. }

  22. // Update uptime timer
  23. function uptime() {
  24.   clearInterval(timer_uptime);
  25.   var uptime = document.getElementById("uptime");
  26.   var interval = uptime.value;
  27.   if (interval > 0) {
  28.     timer_uptime = setInterval("get_uptime()", 1000 * interval);
  29.   }
  30. }

  31. // Update peek_all timer
  32. function timer() {
  33.   clearInterval(timer_peek_all);
  34.   var timer = document.getElementById("timer");
  35.   interval = timer.value;
  36.   if (interval > 0) {
  37.     timer_peek_all = setInterval("peek_all()", 1000 * interval);
  38.   }
  39. }

  40. // Update number format
  41. function format() {
  42.   var table = document.getElementById("registers");
  43.   var rows = table.rows.length;
  44.   var peek;
  45.   var poke;
  46.   for (var index = 0; index < rows; index++) {
  47.     peek = document.getElementById("peek_" + index);
  48.     if (peek) {
  49.       peek.value = fmtUnsignedLong(parseInt(peek.value));
  50.     }
  51.     poke = document.getElementById("poke_" + index);
  52.     if (poke) {
  53.       poke.value = fmtUnsignedLong(parseInt(poke.value));
  54.     }
  55.   }
  56. }

  57. // Convert unsigned long to dec/hex string
  58. function fmtUnsignedLong(value) {
  59.   var format = document.getElementById("format");
  60.   var hexStr;
  61.   if (format.value == 0) {
  62.     hexStr = value.toString(16).toUpperCase();
  63.     hexStr = "0x" + "00000000".substr(0, 8 - hexStr.length) + hexStr;
  64.   } else {
  65.     hexStr = value.toString(10);
  66.   }
  67.   return hexStr;
  68. }

  69. // Copy peek to poke
  70. function copy(reg) {
  71.   var peek = document.getElementById("peek_" + reg);
  72.   if (peek) {
  73.     var poke = document.getElementById("poke_" + reg);
  74.     if (poke) {
  75.       poke.value = peek.value;
  76.     }
  77.   }
  78. }

  79. // Copy all peek to poke
  80. function copy_all() {
  81.   var table = document.getElementById("registers");
  82.   var rows = table.rows.length - 1;
  83.   for (var index = 0; index < rows; index++) {
  84.     copy(index);
  85.   }
  86. }

  87. // Peek all addresses
  88. function peek_all() {
  89.   var table = document.getElementById("registers");
  90.   var rows = table.rows.length - 1;
  91.   for (var index = 0; index < rows; index++) {
  92.     peek(index);
  93.   }
  94. }

  95. // Poke all addresses
  96. function poke_all() {
  97.   var table = document.getElementById("registers");
  98.   var rows = table.rows.length - 1;
  99.   for (var index = 0; index < rows; index++) {
  100.     poke(index);
  101.   }
  102. }

  103. // Add row to table
  104. function add_register(reg) {
  105.   var table = document.getElementById("registers");
  106.   var next = table.rows.length - 1;
  107.   var row = table.insertRow(-1);
  108.   var newcell;
  109.   var addr = 0x41200000;
  110.   if (next > 0) {
  111.     addr = parseInt(document.getElementById("addr_" + (next - 1)).value) + 4;
  112.   }
  113.   newcell = row.insertCell(0);
  114.   newcell.innerHTML = '<input type="text" id="addr_' + next + '" value="' + fmtUnsignedLong(addr) + '" size="10">';
  115.   newcell = row.insertCell(1);
  116.   newcell.innerHTML = '<input type="text" id="peek_' + next + '" value="0x00000000" size="10" readonly="readonly"></td>';
  117.   newcell = row.insertCell(2);
  118.   newcell.innerHTML = '<input type="submit" value="Peek" onclick="peek(' + next + ')">';
  119.   newcell = row.insertCell(3);
  120.   newcell.innerHTML = '<span id="speek_' + next + '">-</span>';
  121.   newcell = row.insertCell(4);
  122.   newcell.innerHTML = '<input type="submit" value=">>" onclick="copy(' + next + ')">';
  123.   newcell = row.insertCell(5);
  124.   newcell.innerHTML = '<input type="text" id="poke_' + next + '" value="0x00000000" size="10">';
  125.   newcell = row.insertCell(6);
  126.   newcell.innerHTML = '<input type="submit" value="Poke" onclick="poke(' + next + ')">';
  127.   newcell = row.insertCell(7);
  128.   newcell.innerHTML = '<span id="spoke_' + next + '">-</span>';
  129.   newcell = row.insertCell(8);
  130.   newcell.innerHTML = '<input type="text" id="name_' + next + '" value="Register @ ' + fmtUnsignedLong(addr) + '" size="40">';
  131. }

  132. // Remove all rows from table
  133. function remove_all() {
  134.   var table = document.getElementById("registers");
  135.   var rows = table.rows.length - 1;
  136.   for (var index = 0; index < rows; index++) {
  137.     table.deleteRow(-1);
  138.   }
  139. }

  140. // Remove row from table
  141. function rem_register(reg) {
  142.   var table = document.getElementById("registers");
  143.   if (table.rows.length > 1) {
  144.     table.deleteRow(-1);
  145.   }
  146. }

  147. // Poke address & display result
  148. function poke(reg) {
  149.   var url = "/cgi-bin/poke?" + document.getElementById("addr_" + reg).value + "&" + document.getElementById("poke_" + reg).value;
  150.   if (window.XMLHttpRequest) {
  151.     var ajaxReq = new XMLHttpRequest();
  152.     ajaxReq.onreadystatechange = function() {
  153.       if (ajaxReq.readyState == 4 && ajaxReq.status == 200) {
  154.         var respText = ajaxReq.responseText;
  155.         if (respText.substr(0,6) == "Error:") {
  156.           document.getElementById("spoke_" + reg).innerHTML = "Failed";
  157.         } else {
  158.           document.getElementById("spoke_" + reg).innerHTML = "Success";
  159.         }
  160.       }
  161.     }
  162.     ajaxReq.open("POST", url, true);
  163.     ajaxReq.send(null);
  164.   }
  165. }

  166. // Peek address & display result
  167. function peek(reg) {
  168.   var url = "/cgi-bin/peek?" + document.getElementById("addr_" + reg).value;
  169.   if (window.XMLHttpRequest) {
  170.     var ajaxReq = new XMLHttpRequest();
  171.     ajaxReq.onreadystatechange = function() {
  172.       if (ajaxReq.readyState == 4 && ajaxReq.status == 200) {
  173.         var respText = ajaxReq.responseText;
  174.         if (respText.substr(0,6) == "Error:") {
  175.           document.getElementById("speek_" + reg).innerHTML = "Failed";
  176.         } else {
  177.           document.getElementById("speek_" + reg).innerHTML = "Success";
  178.           document.getElementById("peek_" + reg).value = fmtUnsignedLong(parseInt(respText));
  179.         }
  180.       }
  181.     }
  182.     ajaxReq.open("POST", url, true);
  183.     ajaxReq.send(null);
  184.   }
  185. }

  186. // Note: browser file access is made difficult for security reasons - there maybe a better way of doing file read & write.

  187. var config_file = null;

  188. // Create virtual configuration file from address table for user to download
  189. function create_config() {
  190.   var text = "";
  191.   var table = document.getElementById("registers");
  192.   var rows = table.rows.length - 1;
  193.   for (var index = 0; index < rows; index++) {
  194.     var addr = document.getElementById("addr_" + index).value;
  195.     var poke = document.getElementById("poke_" + index).value;
  196.     var name = document.getElementById("name_" + index).value;
  197.     text += addr + "|" + poke + "|" + name + "\n";
  198.   }
  199.   const data = new Blob([text], {type: 'text/plain'});
  200.   if (config_file !== null) {
  201.     URL.revokeObjectURL(config_file);
  202.   }
  203.   config_file = URL.createObjectURL(data);
  204.   var link = document.getElementById('download');
  205.   link.href = config_file;
  206.   link.style.display = 'inline';
  207. }

  208. // Read configuration file and update address table
  209. function load_config(input) {
  210.   var file = input.target.files[0];
  211.   if (file) {
  212.     var reader = new FileReader();
  213.     reader.onload = function(input) {
  214.       var contents = input.target.result;
  215.       const lines = contents.split(/\r\n|\n/);
  216.       remove_all();
  217.       lines.forEach((line) => {
  218.         if (line.length > 0) {
  219.           var table = document.getElementById("registers");
  220.           var next = table.rows.length - 1;
  221.           add_register();
  222.           const values = line.split("|");
  223.           document.getElementById("addr_" + next).value = values[0];
  224.           document.getElementById("poke_" + next).value = values[1];
  225.           document.getElementById("name_" + next).value = values[2];
  226.         }
  227.       });
  228.     };
  229.     reader.readAsText(file);
  230.   }
  231. }

  232. document.getElementById('load_config').addEventListener('change', load_config, false);

17. Quick check enhanced website is working as expected

Copy the modified files over to PetaLinux to check they work.
steve@Linux-Steve:/home/steve/projects/petalinux$ scp project-spec/meta-user/recipes-apps/website/files/cgi-bin/index.cgi root@192.168.2.87:/srv/www/cgi-bin
steve@Linux-Steve:/home/steve/projects/petalinux$ scp project-spec/meta-user/recipes-apps/website/files/uptime.js root@192.168.2.87:/srv/www
Using a web browser access the Zedboard Webserver to check out the new features on the webpage.

The controls in the Peek & Poke section of the enhanced website are as follows :- Missing Image! Create the above address table by using the Add button to add new registers, the Address column to edit the address locations, the Poke Value column to edit the values to poke and the Description column to provide a description of what the registers are used for. Poke the LED's register and Poke two registers inside the Register Bank at addresses 0x43C00000 and 0x43C00008. Peek All to peek all the registers shown in the table.

Create a configuration file from the address table by clicking on Create.... A generated link (config.txt) should now appear next to the Create... button. Right click on the link and select Save Link As..., set the name to leds_switches.txt and save the file.

18. Rebuild PetaLinux

Both the FPGA bitstream and enhanced website are currently soft-loaded for test purposes. Since the testing as proven successful the PetaLinux project can be rebuild to include the updated files.
steve@Linux-Steve:/home/steve/projects/petalinux$ petalinux-config --get-hw-description ../leds_switches/fw/system_wrapper.xsa
steve@Linux-Steve:/home/steve/projects/petalinux$ petalinux-build
steve@Linux-Steve:/home/steve/projects/petalinux$ petalinux-package --prebuilt --force

19. Deploy PetaLinux on Zedboard

Reload PetaLinux to double check it now contains the updated files.
steve@Linux-Steve:/home/steve/projects/petalinux$ petalinux-boot --jtag --prebuilt 3

20. Check everything is working as expected

All being well the following sequence of events should be observed, pay particular attention to point 2.
  1. The blue done LED illuminates indicating the Programmable Logic (PL) has been programmed.
  2. LED 7 and 0 illuminate indicating the bitstream from the enhanced leds_switches project is in use.
  3. The software runs on the Processor System (PS).
  4. PetaLinux starts to boot.
  5. The led-runner application launches and executes the expanding & contracting LED illumination sequence.
  6. The PetaLinux login prompt appears in the terminal emulator.
Using a web browser access the Zedboard Webserver and reload the saved configuration by clicking on Browse... and selecting leds_switches.txt.

21. Files for revision control

The following files/directories are the new ones to add to the repository.