cccamp-2019/index.html

814 lines
23 KiB
HTML
Raw Normal View History

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Fomu: An FPGA in your USB Port</title>
<meta name="description" content="A framework for easily creating beautiful presentations using HTML">
<meta name="author" content="Sean &quot;xobs&quot; Cross">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="stylesheet" href="css/reveal.css">
<link rel="stylesheet" href="css/theme/teardown19.css" id="theme">
<!-- Theme used for syntax highlighting of code -->
<link rel="stylesheet" href="lib/css/zenburn.css">
<!-- Printing and PDF exports -->
<script>
var link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = window.location.search.match(/print-pdf/gi) ? 'css/print/pdf.css' : 'css/print/paper.css';
document.getElementsByTagName('head')[0].appendChild(link);
</script>
<!--[if lt IE 9]>
<script src="lib/js/html5shiv.js"></script>
<![endif]-->
<style>
/*********************************************
* ZOOM REVERSE TRANSITION (i.e. zoom out)
*********************************************/
.reveal .slides section[data-transition=zoomrev],
.reveal.zoomrev .slides section:not([data-transition]) {
transition-timing-function: ease;
}
.reveal .slides>section[data-transition=zoomrev].past,
.reveal .slides>section[data-transition~=zoomrev-out].past,
.reveal.zoomrev .slides>section:not([data-transition]).past {
visibility: hidden;
-webkit-transform: scale(0.2);
transform: scale(0.2);
}
.reveal .slides>section[data-transition=zoomrev].future,
.reveal .slides>section[data-transition~=zoomrev-in].future,
.reveal.zoomrev .slides>section:not([data-transition]).future {
visibility: hidden;
-webkit-transform: scale(16);
transform: scale(16);
}
.reveal .slides>section>section[data-transition=zoomrev].past,
.reveal .slides>section>section[data-transition~=zoomrev-out].past,
.reveal.zoomrev .slides>section>section:not([data-transition]).past {
-webkit-transform: translate(0, 150%);
transform: translate(0, 150%);
}
.reveal .slides>section>section[data-transition=zoomrev].future,
.reveal .slides>section>section[data-transition~=zoomrev-in].future,
.reveal.zoomrev .slides>section>section:not([data-transition]).future {
-webkit-transform: translate(0, -150%);
transform: translate(0, -150%);
}
</style>
</head>
<body>
<!-- Start of main presentation -->
<div class="reveal">
<div class="footer">
<div class="url">Slides: <a href="https://p.xobs.io/camp19/">p.xobs.io/camp19</a> &nbsp; Software: <a
href="https://fomu.im/camp19">fomu.im/camp19</a></div>
<span class="theme">CCC Camp 2019</span><span class="hashtag"> | #CCCamp</span><span class="twitter"> |
@tomu_im</span>
</div>
<div class="slides">
<section>
<p>
Software is available on the USB drive marked "Fomu Workshop"
</p>
<p>
Or, download software from <a href="https://fomu.im/camp19">fomu.im/camp19</a>
</p>
</section>
<section data-background-image="css/theme/teardown2019-title-bg-transparent.svg">
<h1>Fomu: An FPGA in your USB Port</h1>
<h4>A whirlwind introduction to Fomu; a workshop in three levels</h4>
<p align="right">
<small>Sean "xobs" Cross - <a href="https://xobs.io/">https://xobs.io/</a> - @xobs</small>
<small>Tim "mithro" Ansell - <a
href="https://github.com/timvideos/litex-buildenv/wiki/">https://github.com/timvideos/litex-buildenv/wiki/</a>
- @mithro</small>
</p>
</section>
<section>
<h2>Levels of Fomu</h2>
<p>
Fomu aims to be accessable on three levels:
<ol>
<li>Python / Interpreter</li>
<li>RISC-V / C</li>
<li>FPGA / HDL</li>
</ol>
</p>
<aside class="notes">
The overarching idea behind Fomu is to take a top-down approach to hardware design. Start at
something familiar, such as Python, and keep drilling down until you are no longer interested in
going further.
</aside>
</section>
<section>
<h2>Workshop Outline</h2>
<ol>
<li>What do I need to get started?</li>
<li>What is an FPGA, and what is Fomu?</li>
<li>Working with Fomu using Python, RISC-V, and HDL</li>
</ol>
<aside class="notes">
Broadly, this workshop will cover three parts: What do you need to get started with Fomu, what is an
FPGA and why is Fomu special, and finally how to work with Fomu at three different levels.
</aside>
</section>
<section>
<section>
<h2>What do I need to get started?</h2>
<ol>
<li>DFU utilities</li>
<li>Serial console</li>
<li>RISC-V toolchain</li>
<li>FPGA Toolchain</li>
<li>Python 3</li>
</ol>
<aside class="notes">
You require all of the software on this list. This software is provided on the USB drive that's
provided, or you can get them from <a
href="https://github.com/im-tomu/fomu-toolchain/releases">https://github.com/im-tomu/fomu-toolchain/releases</a>.
</aside>
</section>
</section>
<section>
<section>
<h2>What is an FPGA?</h2>
<img data-src="img/ice40-lut.png" alt="SB_LUT4">
<aside class="notes">
An FPGA is an array of gates that's field-programmable. A more useful definition might be "a
chip you can reconfigure". Most chips are collections of transistors that take two inputs and
have one output. FPGAs have collections of transistors that look like this -- they take multiple
inputs and produce multiple outputs. On Fomu, the basic building block is a 4-input 1-output
lookup table called an "LC4". These are so important to FPGAs that the part number usually
contains how many LUTs there are. The Fomu has a UP5K, which has about 5000 LUTs. The NeTV had
an LX9, which had 9000 LUTs. The NeTV2 has an XC7A35T, which has 35000 LUTs.
</aside>
</section>
<section>
<h2>What is an FPGA?</h2>
<table style="transform: scale(.80) translate(-15%)">
<tr>
<th></th>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
<th>5</th>
<th>6</th>
<th>7</th>
<th>8</th>
<th>9</th>
<th>10</th>
<th>11</th>
<th>12</th>
<th>13</th>
<th>14</th>
<th>15</th>
</tr>
<tr>
<td>IO0</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td>1</td>
<td>1</td>
<td>1</td>
<td>1</td>
<td>1</td>
<td>1</td>
<td>1</td>
<td>1</td>
</tr>
<tr>
<td>IO1</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td>1</td>
<td>1</td>
<td>1</td>
<td>1</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td>1</td>
<td>1</td>
<td>1</td>
<td>1</td>
</tr>
<tr>
<td>IO2</td>
<td>0</td>
<td>0</td>
<td>1</td>
<td>1</td>
<td>0</td>
<td>0</td>
<td>1</td>
<td>1</td>
<td>0</td>
<td>0</td>
<td>1</td>
<td>1</td>
<td>0</td>
<td>0</td>
<td>1</td>
<td>1</td>
</tr>
<tr>
<td>IO3</td>
<td>0</td>
<td>1</td>
<td>0</td>
<td>1</td>
<td>0</td>
<td>1</td>
<td>0</td>
<td>1</td>
<td>0</td>
<td>1</td>
<td>0</td>
<td>1</td>
<td>0</td>
<td>1</td>
<td>0</td>
<td>1</td>
</tr>
<tr>
<td>O</td>
<td>?</td>
<td>?</td>
<td>?</td>
<td>?</td>
<td>?</td>
<td>?</td>
<td>?</td>
<td>?</td>
<td>?</td>
<td>?</td>
<td>?</td>
<td>?</td>
<td>?</td>
<td>?</td>
<td>?</td>
<td>?</td>
</tr>
</table>
<aside class="notes">
A LUT is a lookup table. The value of the output depends on the value of all the inputs. You can
make arbitrary boolean logic here. For example, we could make a LUT that performs the logical
AND of all the inputs, and set it so that it only outputs a "1" if all of the inputs are 1. Or
we can make a NAND gate by taking the inverse. Each one of those 5000 LUTs in Fomu has a truth
table like this that it evaluates in real time.
</aside>
</section>
<section>
<h2>What is an FPGA?</h2>
<pre><code class="verilog" data-trim>
module example (output reg [0:5] Q, input C);
reg [0:8] counter;
always @(posedge C)
begin
counter <= counter + 1'b1;
Q = counter[7] ^ counter[5] | counter<<2;
end
endmodule
</code></pre>
<img class="fragment" data-src="img/verilog-synthesis.png" alt="Verilog Synthesis">
<aside class="notes">
It gets really tedious to be thinking about lookup tables all the time, so humans created
programming languages to do it for them. This is an example of Verilog code. It makes a simple
counter that outputs the xor of some values.
In order to turn this Verilog code into actual LUTs, we run it through a synthesizer. Much like
how a compiler turns programming language into CPU opcodes, a synthesizer turns Verilog code
into lookup tables
</aside>
</section>
<section>
<h2>About the ICE40UP5K</h2>
<ol>
<li>5280 4-input LUTs (LC)</li>
<li>16 kilobytes BRAM</li>
<li class="fragment highlight-blue">128 kilobytes "SPRAM"</li>
<li>Current-limited 3-channel LED driver</li>
<li>2x I2C and 2x SPI</li>
<li>8 16-bit DSP units</li>
<li class="fragment highlight-blue">Warmboot capability</li>
<li class="fragment highlight-blue">Open toolchain</li>
</ol>
</section>
<section>
<h2>What is Fomu?</h2>
<ul>
<li>ICE40UP5K</li>
<li>2MB QSPI flash memory</li>
<li>Four edge-plated pads</li>
<li>ESD protection</li>
<li>USB implemented in HDL</li>
<li class="fragment highlight-blue">Fits in your USB port</li>
</ul>
<aside class="notes">
Fomu is an FPGA that fits in your USB port. It has foru buttons, 2 MB of SPI flash, an RGB LED,
and an ICE40UP5K with 5280 LCs. It also has 128 kB of dedicated RAM, not counting the block RAM.
Unlike many other PCBs, Fomu does not have a separate USB controller chip. This means that any
projects that want to use the USB port must include a USB softcore.
</aside>
</section>
<section>
<h2>Fomu Block Design Diagram</h2>
<img data-src="img/fomu-block-diagram.png" alt="Fomu block diagram">
</section>
<section>
<h2>Fomu SPI Flash Layout</h2>
<img data-src="img/fomu-memory-layout.png" alt="Fomu memory layout">
<!-- <ol>
<li>Bootloader</li>
<li>Recovery</li>
<li>Magic constants</li>
<li>Interpreters</li>
<li>Updates</li>
</ol> -->
</section>
</section>
<section>
<section>
<h2>Working with Fomu</h2>
</section>
<section>
<h2>FAT Bootloader</h2>
<ul>
<li>Presents itself as a USB disk</li>
<li>Drag and drop files to program</li>
<li>Multiple interpreter support</li>
</ul>
<div>
<img data-src="img/under-construction.gif" class="fragment">
</div>
</section>
<section>
<h2>"fail safe" bootloader</h2>
Device Firmware Update - <strong>DFU</strong>
</section>
<section>
<h2>Uploading Code</h2>
<pre><code>$ dfu-util -l
Found DFU: [1209:5bf0] name="Fomu Hacker running DFU Bootloader v1.8.8"
$ dfu-util -D program.bin
Download [========= ] 36% 38912 bytes
Download done.
$ dfu-util -l
$</code></pre>
</section>
</section>
<section>
<section>
<h2>Python / Interpreted</h2>
<ol>
<li><strong>Goal:</strong> Multiple interpreters, auto-reload, USB disk interface</li>
<li><strong>Now:</strong> MicroPython binary</li>
</ol>
</section>
<section>
<h2>Loading Programs onto Fomu</h2>
<pre><code>
$ dfu-util -l
Found DFU: [1209:5bf0] name="Fomu DFU Bootloader v1.8.8"
$ dfu-util -e # Boot current program
$ dfu-util -D new-image.dfu # Load new program</code></pre>
<h3 class="fragment">u<code>5b f0</code>mu</h3>
</section>
<section>
<h2>Loading MicroPython</h2>
<pre><code>$ dfu-util -D micropython-fomu.dfu</code></pre>
</section>
<section>
<h2>Connecting via serial</h2>
<pre class="fragment"><code>screen /dev/cu.usbserial*</code></pre>
<pre class="fragment"><code>screen /dev/ttyACM*</code></pre>
<pre class="fragment"><code>Tera Term</code></pre>
<pre class="fragment"><code>MicroPython v1.10-296-g0a5a77a on 2019-06-18; fomu with vexriscv
>>></code></pre>
</section>
<section>
<h2>Interacting with Fomu</h2>
<pre><code class="python" data-trim>
>>> import fomu
>>> rgb = fomu.rgb()
>>> rgb.mode("error")
>>>
</code></pre>
</section>
<section>
<h2>Read SPI ID</h2>
<pre><code class="python" data-trim>
>>> spi = fomu.spi()
>>> hex(spi.id())
'0xc2152815'
>>>
</code></pre>
</section>
<section>
<h2>Memory-Mapped Registers</h2>
<pre><code class="cpp">#define CSR_VERSION_MAJOR_ADDR 0xe0007000
#define CSR_VERSION_MINOR_ADDR 0xe0007004
#define CSR_VERSION_REVISION_ADDR 0xe0007008</code></pre>
<pre class="fragment"><code class="python">>>> import machine
>>> machine.mem32[0xe0007000]
1
>>></code></pre>
</section>
<section>
<h2>RGB LED Driver reference</h2>
<img data-src="img/ice40-ledd.png" alt="ICE40 LEDD registers">
<pre class="fragment"><code class="python" data-trim>>>> rgb.write_raw(0b0001, 255)
>>> rgb.write_raw(0b1010, 14)
>>> rgb.write_raw(0b1011, 1)
>>> </code></pre>
</section>
<section>
<h2>Future Work</h2>
<ul>
<li>CircuitPython</li>
<li>eLua</li>
<li>Espurino?</li>
</ul>
</section>
</section>
<section>
<section>
<h2>RISC-V</h2>
</section>
<section>
<h2>LiteX Model</h2>
<img data-src="img/litex-design.png" alt="LiteX Design">
</section>
<section>
<h2>Wishbone Bridge</h2>
<img data-src="img/wishbone-usb-debug-bridge.png" alt="Wishbone bridge">
</section>
<section>
<h2>CPU is Optional</h2>
<ul>
<li>Multiple CPUs available</li>
<li>
<ul>
<li>VexRiscv</li>
<li>picorv32</li>
<li>lm32</li>
<li>...</li>
</ul>
</li>
<li>Also works just fine with no CPU</li>
</ul>
</section>
<section>
<h2>CSR Access</h2>
<pre><code class="cpp">#define CSR_VERSION_MAJOR_ADDR 0xe0007000
#define CSR_VERSION_MAJOR_SIZE 1
#define CSR_VERSION_MINOR_ADDR 0xe0007004
#define CSR_VERSION_MINOR_SIZE 1
#define CSR_VERSION_REVISION_ADDR 0xe0007008
#define CSR_VERSION_REVISION_SIZE 1
#define CSR_VERSION_GITREV_ADDR 0xe000700c
#define CSR_VERSION_GITREV_SIZE 4
#define CSR_VERSION_GITEXTRA_ADDR 0xe000701c
#define CSR_VERSION_GITEXTRA_SIZE 2
</code></pre>
Excerpt from <code>csr.h</code>
</section>
<section>
<h2>Reading CPU Version</h2>
<pre><code class="sh">$ wishbone-tool --pid 0x5bf0 0xe0007000
Value at e0007000: 00000001
$ wishbone-tool --pid 0x5bf0 0xe0007004
Value at e0007004: 00000008
$ wishbone-tool --pid 0x5bf0 0xe0007008
Value at e0007008: 00000001</code></pre>
</section>
<section>
<h2>Interacting with LEDD directly</h2>
<img data-src="img/ice40-ledd.png" alt="ICE40 LEDD registers">
<pre class="fragment"><code class="cpp">#define CSR_RGB_DAT_ADDR 0xe0006800L
#define CSR_RGB_ADDR_ADDR 0xe0006804L</code></pre>
<pre class="fragment"><code>$ wishbone-tool --pid 0x5bf0 0xe0006804 1
$ wishbone-tool --pid 0x5bf0 0xe0006800 0xff</code></pre>
</section>
<section>
<h2>Writing RISC-V Code</h2>
<pre><code>$ make
CC ./src/main.c main.o
CC ./src/rgb.c rgb.o
CC ./src/time.c time.o
AS ./src/crt0-vexriscv.S crt0-vexriscv.o
LD riscv-blink.elf
OBJCOPY riscv-blink.bin
IHEX riscv-blink.ihex
$ </code></pre>
<p>
From <code>riscv-blink</code> directory in <code>teardown2019-workshop</code>
</p>
</section>
<section>
<h2>Modifying RISC-V Code</h2>
<pre><code class="diff">--- a/riscv-blink/src/main.c
+++ b/riscv-blink/src/main.c
@@ -38,6 +38,7 @@ void isr(void) {
void main(void) {
rgb_init();
irq_setie(0);
+ rgb_write((100000/64000)-1, LEDDBR);
int i = 0;
while (1) {
i++;</code></pre>
</section>
<section>
<h2>Other RISC-V Programs</h2>
riscv-usb-cdcacm: echo characters back after adding 1
</section>
</section>
<section>
<section>
<h2>Hardware Description Language</h2>
</section>
<section>
<h2>Yosys and NextPNR</h2>
<ul>
<li>Timing Driven!</li>
</ul>
<pre><code>Max frequency for clock 'clk12': 24.63 MHz (PASS at 12.00 MHz)
Max frequency for clock 'clk48_1': 60.66 MHz (PASS at 48.00 MHz)
Max frequency for clock 'clkraw': 228.05 MHz (PASS at 48.00 MHz)</code></pre>
</section>
<section>
<h2>Blinking an LED</h2>
<pre><code>$ make FOMU_REV=evt
...
20 warnings, 0 errors
PACK blink.bin
Built 'blink' for Fomu evt1
$ dfu-util -D blink.bin</code></pre>
</section>
<section>
<h2>LiteX and MiGen</h2>
<ol>
<li>Define hardware in Python</li>
<li>Evaluate Python to produce netlist</li>
<li>Synthesize netlist to FPGA</li>
</ol>
</section>
<section>
<h2>lxbuildenv.py</h2>
<ol>
<li>Python environment using native interpreter</li>
<li>Very stable, good for hardware projects</li>
<li>Should work with system Python</li>
<li>Runs on Linux, Windows, Raspberry Pi</li>
</ol>
</section>
<section>
<h2>Why do we need a CPU?</h2>
<img data-src="img/litex-design.png" alt="LiteX Design">
</section>
<section>
<h2>What if we remove the CPU?</h2>
<ul>
<li>Workshop project has no CPU</li>
<li>DummyUsb module automatically enumerates</li>
<li>Wishbone Debug Bridge still accessible</li>
</ul>
</section>
<section>
<h2>Build Workshop Module</h2>
<pre><code>$ python3 workshop.py --placer heap
...
5 warnings, 0 errors
$ </code></pre>
</section>
<section>
<h2>Load onto Fomu</h2>
<pre><code>$ dfu-util -D build/gateware/top.bin
Download [=========================] 100% 104090 bytes
Download done.
$ </code></pre>
</section>
<section>
<h2>Write a value to RAM</h2>
<pre><code>$ wishbone-tool --pid 0x5bf0 0x10000000
Value at 10000000: 0baf801e
$ wishbone-tool --pid 0x5bf0 0x10000000 0x12345678
$ wishbone-tool --pid 0x5bf0 0x10000000
Value at 10000000: 12345678
$ </code></pre>
</section>
<section>
<h2>Adding Hardware</h2>
<img data-src="img/ice40-rgb.jpg" alt="Schematic of RGB block">
</section>
<section>
<h2>Technology Library Reference</h2>
<pre><code class="verilog">// Verilog Instantiation
SB_RGBA_DRV RGBA_DRIVER (
.CURREN(ENABLE_CURR),
.RGBLEDEN(ENABLE_RGBDRV),
.RGB0PWM(RGB0),
.RGB1PWM(RGB1),
.RGB2PWM(RGB2),
.RGB0(LED0),
.RGB1(LED1),
.RGB2(LED2)
);
defparam RGBA_DRIVER.CURRENT_MODE = "0b0";
defparam RGBA_DRIVER.RGB0_CURRENT = "0b111111";
defparam RGBA_DRIVER.RGB1_CURRENT = "0b111111" ;
defparam RGBA_DRIVER.RGB2_CURRENT = "0b111111";</code></pre>
<p>SBTICETechnologyLibrary201504.pdf page 147</p>
</section>
<section>
<h2>RGB Block</h2>
<pre><code class="python" style="font-size: 18px; line-height: 22px">class FomuRGB(Module, AutoCSR):
def __init__(self, pads):
self.output = CSRStorage(3)
self.specials += Instance("SB_RGBA_DRV",
i_CURREN = 0b1,
i_RGBLEDEN = 0b1,
i_RGB0PWM = self.output.storage[0],
i_RGB1PWM = self.output.storage[1],
i_RGB2PWM = self.output.storage[2],
o_RGB0 = pads.r,
o_RGB1 = pads.g,
o_RGB2 = pads.b,
p_CURRENT_MODE = "0b1",
p_RGB0_CURRENT = "0b000011",
p_RGB1_CURRENT = "0b000011",
p_RGB2_CURRENT = "0b000011",
)</code></pre>
</section>
<section>
<h2>Instantiating FomuRGB</h2>
<pre><code class="diff">@@ -55,6 +75,10 @@ class BaseSoC(SoCCore):
with_ctrl=False,
**kwargs)
+ # Add the LED driver block
+ led_pads = platform.request("rgb_led")
+ self.submodules.rgb = FomuRGB(led_pads)
+
# UP5K has single port RAM....
# Use this as CPU RAM.
spram_size = 128*1024</code></pre>
</section>
<section>
<h2>Interacting with the CSR</h2>
<pre><code>csr_register,rgb_output,0xe0006800,1,rw</code></pre>
<p>From <code>test/csr.csv</code></p>
</section>
<section>
<h2>VexRiscv</h2>
</section>
</section>
<section>
<h2>Thank you</h2>
<h2>Lunch Time!</h2>
</section>
</div>
</div> <!-- class="reveal" -->
<!-- End of main presentation -->
<!-- Start of configuration section -->
<script src="lib/js/head.min.js"></script>
<script src="js/reveal.js"></script>
<script>
var presenter = !!Reveal.getQueryHash().s;
// More info https://github.com/hakimel/reveal.js#configuration
Reveal.initialize({
controls: presenter ? false : true,
progress: true,
history: true,
center: true,
controlsTutorial: presenter ? false : true,
slideNumber: presenter ? null : 'c/t',
// The "normal" size of the presentation, aspect ratio will be preserved
// when the presentation is scaled to fit different resolutions. Can be
// specified using percentage units.
width: 960,
height: 700,
// Factor of the display size that should remain empty around the content
margin: 0.1,
multiplex: {
url: 'https://p.xobs.io/',
id: 'cbd6556886c2825d',
secret: Reveal.getQueryHash().s || null
},
// Bounds for smallest/largest possible scale to apply to content
minScale: 0.02,
maxScale: 5.5,
transition: 'slide', // none/fade/slide/convex/concave/zoom
// More info https://github.com/hakimel/reveal.js#dependencies
dependencies: [
{ src: 'lib/js/classList.js', condition: function () { return !document.body.classList; } },
{ src: 'plugin/markdown/marked.js', condition: function () { return !!document.querySelector('[data-markdown]'); } },
{ src: 'plugin/markdown/markdown.js', condition: function () { return !!document.querySelector('[data-markdown]'); } },
{ src: 'plugin/highlight/highlight.js', async: true, callback: function () { hljs.initHighlightingOnLoad(); } },
{ src: 'plugin/search/search.js', async: true },
{ src: 'plugin/zoom-js/zoom.js', async: true },
{ src: 'plugin/notes/notes.js', async: true },
{ src: 'lib/js/socket.io.js', async: true },
{
src: presenter ?
'plugin/multiplex/master.js' :
'plugin/multiplex/client.js', async: true
},
]
});
</script>
</body>
</html>