Skip to content

Commit be0786a

Browse files
authored
Add files via upload
1 parent dd4da27 commit be0786a

16 files changed

+6031
-0
lines changed

.gitignore

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/tests/*.o
2+
/firmware/*.o
3+
/firmware/firmware.bin
4+
/firmware/firmware.elf
5+
/firmware/firmware.hex
6+
/firmware/firmware.map
7+
/dhrystone/dhry.bin
8+
/dhrystone/dhry.elf
9+
/dhrystone/dhry.hex
10+
/dhrystone/dhry.map
11+
/dhrystone/testbench.vvp
12+
/dhrystone/testbench.vcd
13+
/dhrystone/testbench_nola.vvp
14+
/dhrystone/testbench_nola.vcd
15+
/dhrystone/timing.vvp
16+
/dhrystone/timing.txt
17+
/dhrystone/*.d
18+
/dhrystone/*.o
19+
/riscv-gnu-toolchain-riscv32i
20+
/riscv-gnu-toolchain-riscv32ic
21+
/riscv-gnu-toolchain-riscv32im
22+
/riscv-gnu-toolchain-riscv32imc
23+
/testbench.vvp
24+
/testbench_wb.vvp
25+
/testbench_ez.vvp
26+
/testbench_sp.vvp
27+
/testbench_rvf.vvp
28+
/testbench_synth.vvp
29+
/testbench.gtkw
30+
/testbench.vcd
31+
/testbench.trace
32+
/testbench_verilator*
33+
/check.smt2
34+
/check.vcd
35+
/synth.log
36+
/synth.v
37+
.*.swp
38+
testbench_mod.vvp
39+

COL_IDCT.v

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
module COL_IDCT( input clk,
2+
input signed [31:0] col_idct_ip[0:7],
3+
output signed [31:0] col_idct_op[0:8]
4+
);
5+
reg signed [31:0] x [0:8];
6+
reg signed [31:0] y [0:8];
7+
integer W1 = 2841;
8+
integer W2 = 2676;
9+
integer W3 = 2408;
10+
integer W5 = 1609;
11+
integer W6 = 1108;
12+
integer W7 = 565;
13+
assign col_idct_op [0:8] = y[0:8];
14+
always @(*)
15+
begin
16+
x[0] = col_idct_ip[0];
17+
x[1] = col_idct_ip[1];
18+
x[2] = col_idct_ip[2];
19+
x[3] = col_idct_ip[3];
20+
x[4] = col_idct_ip[4];
21+
x[5] = col_idct_ip[5];
22+
x[6] = col_idct_ip[6];
23+
x[7] = col_idct_ip[7];
24+
x[0] = (x[0] <<< 8) + 8192;
25+
x[8] = W7 * (x[4] + x[5]) + 4;
26+
x[4] = (x[8] + (W1 - W7) * x[4]) >>> 3;
27+
x[5] = (x[8] - (W1 + W7) * x[5]) >>> 3;
28+
x[8] = W3 * (x[6] + x[7]) + 4;
29+
x[6] = (x[8] - (W3 - W5) * x[6]) >>> 3;
30+
x[7] = (x[8] - (W3 + W5) * x[7]) >>> 3;
31+
x[8] = x[0] + x[1];
32+
x[0] = x[0]-x[1];
33+
x[1] = W6 * (x[3] + x[2]) + 4;
34+
x[2] = (x[1] - (W2 + W6) * x[2]) >>> 3;
35+
x[3] = (x[1] + (W2 - W6) * x[3]) >>> 3;
36+
x[1] = x[4] + x[6];
37+
x[4] = x[4]-x[6];
38+
x[6] = x[5] + x[7];
39+
x[5] = x[5]-x[7];
40+
x[7] = x[8] + x[3];
41+
x[8] = x[8]-x[3];
42+
x[3] = x[0] + x[2];
43+
x[0] = x[0]-x[2];
44+
x[2] = (181 * (x[4] + x[5]) + 128) >>> 8;
45+
x[4] = (181 * (x[4] - x[5]) + 128) >>> 8;
46+
y[0] = x[0];
47+
y[1] = x[1];
48+
y[2] = x[2];
49+
y[3] = x[3];
50+
y[4] = x[4];
51+
y[5] = x[5];
52+
y[6] = x[6];
53+
y[7] = x[7];
54+
y[8] = x[8];
55+
end
56+
endmodule

Makefile

+192
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
2+
RISCV_GNU_TOOLCHAIN_GIT_REVISION = 411d134
3+
RISCV_GNU_TOOLCHAIN_INSTALL_PREFIX = /opt/riscv32
4+
5+
# Give the user some easy overrides for local configuration quirks.
6+
# If you change one of these and it breaks, then you get to keep both pieces.
7+
SHELL = bash
8+
PYTHON = python3
9+
VERILATOR = verilator
10+
ICARUS_SUFFIX =
11+
IVERILOG = iverilog$(ICARUS_SUFFIX)
12+
VVP = vvp$(ICARUS_SUFFIX)
13+
14+
TEST_OBJS = $(addsuffix .o,$(basename $(wildcard tests/*.S)))
15+
FIRMWARE_OBJS = firmware/start.o firmware/irq.o firmware/print.o firmware/hello.o firmware/sieve.o firmware/multest.o firmware/stats.o
16+
GCC_WARNS = -Werror -Wall -Wextra -Wshadow -Wundef -Wpointer-arith -Wcast-qual -Wcast-align -Wwrite-strings
17+
GCC_WARNS += -Wredundant-decls -Wstrict-prototypes -Wmissing-prototypes -pedantic # -Wconversion
18+
TOOLCHAIN_PREFIX = $(RISCV_GNU_TOOLCHAIN_INSTALL_PREFIX)i/bin/riscv32-unknown-elf-
19+
COMPRESSED_ISA = C
20+
21+
# Add things like "export http_proxy=... https_proxy=..." here
22+
GIT_ENV = true
23+
24+
test_mod: testbench_mod.vvp firmware/firmware.hex
25+
$(VVP) -N $<
26+
27+
test: testbench.vvp firmware/firmware.hex
28+
$(VVP) -N $<
29+
30+
test_vcd: testbench.vvp firmware/firmware.hex
31+
$(VVP) -N $< +vcd +trace +noerror
32+
33+
test_rvf: testbench_rvf.vvp firmware/firmware.hex
34+
$(VVP) -N $< +vcd +trace +noerror
35+
36+
test_wb: testbench_wb.vvp firmware/firmware.hex
37+
$(VVP) -N $<
38+
39+
test_wb_vcd: testbench_wb.vvp firmware/firmware.hex
40+
$(VVP) -N $< +vcd +trace +noerror
41+
42+
test_ez: testbench_ez.vvp
43+
$(VVP) -N $<
44+
45+
test_ez_vcd: testbench_ez.vvp
46+
$(VVP) -N $< +vcd
47+
48+
test_sp: testbench_sp.vvp firmware/firmware.hex
49+
$(VVP) -N $<
50+
51+
test_axi: testbench.vvp firmware/firmware.hex
52+
$(VVP) -N $< +axi_test
53+
54+
test_synth: testbench_synth.vvp firmware/firmware.hex
55+
$(VVP) -N $<
56+
57+
test_verilator: testbench_verilator firmware/firmware.hex
58+
./testbench_verilator
59+
60+
testbench.vvp: testbench.v picorv32.v
61+
$(IVERILOG) -o $@ $(subst C,-DCOMPRESSED_ISA,$(COMPRESSED_ISA)) $^
62+
chmod -x $@
63+
64+
testbench_mod.vvp: testbench_mod.v axi4_mem_periph.v picorv32.v ROW_IDCT.v COL_IDCT.v
65+
$(IVERILOG) -o $@ $(subst C,-DCOMPRESSED_ISA,$(COMPRESSED_ISA)) $^
66+
chmod -x $@
67+
68+
testbench_rvf.vvp: testbench.v picorv32.v rvfimon.v
69+
$(IVERILOG) -o $@ -D RISCV_FORMAL $(subst C,-DCOMPRESSED_ISA,$(COMPRESSED_ISA)) $^
70+
chmod -x $@
71+
72+
testbench_wb.vvp: testbench_wb.v picorv32.v
73+
$(IVERILOG) -o $@ $(subst C,-DCOMPRESSED_ISA,$(COMPRESSED_ISA)) $^
74+
chmod -x $@
75+
76+
testbench_ez.vvp: testbench_ez.v picorv32.v
77+
$(IVERILOG) -o $@ $(subst C,-DCOMPRESSED_ISA,$(COMPRESSED_ISA)) $^
78+
chmod -x $@
79+
80+
testbench_sp.vvp: testbench.v picorv32.v
81+
$(IVERILOG) -o $@ $(subst C,-DCOMPRESSED_ISA,$(COMPRESSED_ISA)) -DSP_TEST $^
82+
chmod -x $@
83+
84+
testbench_synth.vvp: testbench.v synth.v
85+
$(IVERILOG) -o $@ -DSYNTH_TEST $^
86+
chmod -x $@
87+
88+
testbench_verilator: testbench_mod.v picorv32.v axi4_mem_periph.v testbench.cc
89+
$(VERILATOR) --cc --exe -Wno-lint -trace --top-module picorv32_wrapper testbench_mod.v picorv32.v axi4_mem_periph.v testbench.cc \
90+
$(subst C,-DCOMPRESSED_ISA,$(COMPRESSED_ISA)) --Mdir testbench_verilator_dir
91+
$(MAKE) -C testbench_verilator_dir -f Vpicorv32_wrapper.mk
92+
cp testbench_verilator_dir/Vpicorv32_wrapper testbench_verilator
93+
94+
check: check-yices
95+
96+
check-%: check.smt2
97+
yosys-smtbmc -s $(subst check-,,$@) -t 30 --dump-vcd check.vcd check.smt2
98+
yosys-smtbmc -s $(subst check-,,$@) -t 25 --dump-vcd check.vcd -i check.smt2
99+
100+
check.smt2: picorv32.v
101+
yosys -v2 -p 'read_verilog -formal picorv32.v' \
102+
-p 'prep -top picorv32 -nordff' \
103+
-p 'assertpmux -noinit; opt -fast' \
104+
-p 'write_smt2 -wires check.smt2'
105+
106+
synth.v: picorv32.v scripts/yosys/synth_sim.ys
107+
yosys -qv3 -l synth.log scripts/yosys/synth_sim.ys
108+
109+
# Changing below to use 512k RAM
110+
firmware/firmware.hex: firmware/firmware.bin firmware/makehex.py
111+
$(PYTHON) firmware/makehex.py $< 524288 > $@
112+
113+
firmware/firmware.bin: firmware/firmware.elf
114+
$(TOOLCHAIN_PREFIX)objcopy -O binary $< $@
115+
chmod -x $@
116+
117+
firmware/firmware.elf: $(FIRMWARE_OBJS) $(TEST_OBJS) firmware/sections.lds firmware/nanojpeg.c
118+
$(TOOLCHAIN_PREFIX)gcc -Os -ffreestanding -nostdlib -o $@ \
119+
-Wl,-Bstatic,-T,firmware/sections.lds,-Map,firmware/firmware.map,--strip-debug \
120+
$(FIRMWARE_OBJS) $(TEST_OBJS) -lgcc
121+
chmod -x $@
122+
123+
firmware/start.o: firmware/start.S
124+
$(TOOLCHAIN_PREFIX)gcc -c -march=rv32im$(subst C,c,$(COMPRESSED_ISA)) -o $@ $<
125+
126+
firmware/%.o: firmware/%.c
127+
$(TOOLCHAIN_PREFIX)gcc -c -march=rv32i$(subst C,c,$(COMPRESSED_ISA)) -Os --std=c99 $(GCC_WARNS) -ffreestanding -nostdlib -o $@ $<
128+
129+
tests/%.o: tests/%.S tests/riscv_test.h tests/test_macros.h
130+
$(TOOLCHAIN_PREFIX)gcc -c -march=rv32im -o $@ -DTEST_FUNC_NAME=$(notdir $(basename $<)) \
131+
-DTEST_FUNC_TXT='"$(notdir $(basename $<))"' -DTEST_FUNC_RET=$(notdir $(basename $<))_ret $<
132+
133+
download-tools:
134+
sudo bash -c 'set -ex; mkdir -p /var/cache/distfiles; $(GIT_ENV); \
135+
$(foreach REPO,riscv-gnu-toolchain riscv-binutils-gdb riscv-gcc riscv-glibc riscv-newlib, \
136+
if ! test -d /var/cache/distfiles/$(REPO).git; then rm -rf /var/cache/distfiles/$(REPO).git.part; \
137+
git clone --bare https://github.com/riscv/$(REPO) /var/cache/distfiles/$(REPO).git.part; \
138+
mv /var/cache/distfiles/$(REPO).git.part /var/cache/distfiles/$(REPO).git; else \
139+
(cd /var/cache/distfiles/$(REPO).git; git fetch https://github.com/riscv/$(REPO)); fi;)'
140+
141+
define build_tools_template
142+
build-$(1)-tools:
143+
@read -p "This will remove all existing data from $(RISCV_GNU_TOOLCHAIN_INSTALL_PREFIX)$(subst riscv32,,$(1)). Type YES to continue: " reply && [[ "$$$$reply" == [Yy][Ee][Ss] || "$$$$reply" == [Yy] ]]
144+
sudo bash -c "set -ex; rm -rf $(RISCV_GNU_TOOLCHAIN_INSTALL_PREFIX)$(subst riscv32,,$(1)); mkdir -p $(RISCV_GNU_TOOLCHAIN_INSTALL_PREFIX)$(subst riscv32,,$(1)); chown $$$${USER}: $(RISCV_GNU_TOOLCHAIN_INSTALL_PREFIX)$(subst riscv32,,$(1))"
145+
+$(MAKE) build-$(1)-tools-bh
146+
147+
build-$(1)-tools-bh:
148+
+set -ex; $(GIT_ENV); \
149+
if [ -d /var/cache/distfiles/riscv-gnu-toolchain.git ]; then reference_riscv_gnu_toolchain="--reference /var/cache/distfiles/riscv-gnu-toolchain.git"; else reference_riscv_gnu_toolchain=""; fi; \
150+
if [ -d /var/cache/distfiles/riscv-binutils-gdb.git ]; then reference_riscv_binutils_gdb="--reference /var/cache/distfiles/riscv-binutils-gdb.git"; else reference_riscv_binutils_gdb=""; fi; \
151+
if [ -d /var/cache/distfiles/riscv-gcc.git ]; then reference_riscv_gcc="--reference /var/cache/distfiles/riscv-gcc.git"; else reference_riscv_gcc=""; fi; \
152+
if [ -d /var/cache/distfiles/riscv-glibc.git ]; then reference_riscv_glibc="--reference /var/cache/distfiles/riscv-glibc.git"; else reference_riscv_glibc=""; fi; \
153+
if [ -d /var/cache/distfiles/riscv-newlib.git ]; then reference_riscv_newlib="--reference /var/cache/distfiles/riscv-newlib.git"; else reference_riscv_newlib=""; fi; \
154+
rm -rf riscv-gnu-toolchain-$(1); git clone $$$$reference_riscv_gnu_toolchain https://github.com/riscv/riscv-gnu-toolchain riscv-gnu-toolchain-$(1); \
155+
cd riscv-gnu-toolchain-$(1); git checkout $(RISCV_GNU_TOOLCHAIN_GIT_REVISION); \
156+
git submodule update --init $$$$reference_riscv_binutils_gdb riscv-binutils; \
157+
git submodule update --init $$$$reference_riscv_binutils_gdb riscv-gdb; \
158+
git submodule update --init $$$$reference_riscv_gcc riscv-gcc; \
159+
git submodule update --init $$$$reference_riscv_glibc riscv-glibc; \
160+
git submodule update --init $$$$reference_riscv_newlib riscv-newlib; \
161+
mkdir build; cd build; ../configure --with-arch=$(2) --prefix=$(RISCV_GNU_TOOLCHAIN_INSTALL_PREFIX)$(subst riscv32,,$(1)); make
162+
163+
.PHONY: build-$(1)-tools
164+
endef
165+
166+
$(eval $(call build_tools_template,riscv32i,rv32i))
167+
$(eval $(call build_tools_template,riscv32ic,rv32ic))
168+
$(eval $(call build_tools_template,riscv32im,rv32im))
169+
$(eval $(call build_tools_template,riscv32imc,rv32imc))
170+
171+
build-tools:
172+
@echo "This will remove all existing data from $(RISCV_GNU_TOOLCHAIN_INSTALL_PREFIX)i, $(RISCV_GNU_TOOLCHAIN_INSTALL_PREFIX)ic, $(RISCV_GNU_TOOLCHAIN_INSTALL_PREFIX)im, and $(RISCV_GNU_TOOLCHAIN_INSTALL_PREFIX)imc."
173+
@read -p "Type YES to continue: " reply && [[ "$$reply" == [Yy][Ee][Ss] || "$$reply" == [Yy] ]]
174+
sudo bash -c "set -ex; rm -rf $(RISCV_GNU_TOOLCHAIN_INSTALL_PREFIX){i,ic,im,imc}; mkdir -p $(RISCV_GNU_TOOLCHAIN_INSTALL_PREFIX){i,ic,im,imc}; chown $${USER}: $(RISCV_GNU_TOOLCHAIN_INSTALL_PREFIX){i,ic,im,imc}"
175+
+$(MAKE) build-riscv32i-tools-bh
176+
+$(MAKE) build-riscv32ic-tools-bh
177+
+$(MAKE) build-riscv32im-tools-bh
178+
+$(MAKE) build-riscv32imc-tools-bh
179+
180+
toc:
181+
gawk '/^-+$$/ { y=tolower(x); gsub("[^a-z0-9]+", "-", y); gsub("-$$", "", y); printf("- [%s](#%s)\n", x, y); } { x=$$0; }' README.md
182+
183+
clean:
184+
rm -rf riscv-gnu-toolchain-riscv32i riscv-gnu-toolchain-riscv32ic \
185+
riscv-gnu-toolchain-riscv32im riscv-gnu-toolchain-riscv32imc
186+
rm -vrf $(FIRMWARE_OBJS) $(TEST_OBJS) check.smt2 check.vcd synth.v synth.log \
187+
firmware/firmware.elf firmware/firmware.bin firmware/firmware.hex firmware/firmware.map \
188+
testbench.vvp testbench_sp.vvp testbench_synth.vvp testbench_ez.vvp \
189+
testbench_rvf.vvp testbench_wb.vvp testbench.vcd testbench.trace \
190+
testbench_verilator testbench_verilator_dir
191+
192+
.PHONY: test test_vcd test_sp test_axi test_wb test_wb_vcd test_ez test_ez_vcd test_synth download-tools build-tools toc clean

README.md

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
2+
# Template project
3+
4+
This is a template project for the course EE2003 at IIT Madras.
5+
6+
*Note*: This is a forked repository of PicoRV32 - the original README is available at [[README_picorv32.md]].
7+
8+
Acknowledgements:
9+
10+
- [PicoRV32](https://github.com/YosysHQ/picorv32) - from C. Wolf (YosysHQ).
11+
- [NanoJPEG](https://keyj.emphy.de/nanojpeg/)
12+
13+
14+
## Problem statement
15+
This is a "starter" project that you can use in case you are unable to define a project of your own. It includes the code for the [nanojpeg](https://keyj.emphy.de/nanojpeg/) decoder, with some modifications so that it can be run under the constrained environment of the picorv processor.
16+
17+
This means that you do not have access to things like File Input/Output, memory management (`malloc` etc.) or `printf` style statements that you can usually use for debugging.
18+
19+
To get around these the code has the following additions:
20+
21+
- A set of functions have been defined in `njmem.c` that can allocate memory for random use. It uses a very trivial form of memory allocation that only works because our program never needs to `free` memory and try to use it again later. A set of addresses starting at `0x40000000` are defined for use with the memory management.
22+
- A set of addresses starting from `0x30000000` are defined for reading from the file. You first need to run a pre-processing script (`firmware/jpg2hex.py`) to generate the file `firmware/jpg.hex` which will be mapped to this memory range. This is marked as a read-only memory, so you can only read from that range of addresses. Since the file size cannot be read this way, the script also puts the size of the file as an `int` in the first 4 bytes of the memory range.
23+
- Writing to the address `0x20000000` will result in dumping the appropriate byte into the file `output.dump`. This means that you can use this to do the equivalent of a `fwrite` function in C. However, the filename is always fixed as it cannot be changed from the C program.
24+
- There are also two functions defined in `hello.c` that can be used to read out the number of cycles from the CPU at any point. This can be used, for example, to find out the time taken by the `njDecode` function. More importantly, you can use a similar technique inside your code to get the time taken for other functions and find out which ones take the longest to run.
25+
26+
You will need to read through the code changes in `axi4_mem_periph.v` and `hello.c` etc. in order to understand how all these work. You will need to understand them properly in order to make any changes or improvements in the code.
27+
28+
## How to run
29+
30+
### Step 1 - Generate a suitable input
31+
The code comes with sample data in `firmware/jpg.hex` - this corresponds to the input file `firmware/k8x8.jpg`. The hex file is generated as follows:
32+
```sh
33+
$ cd firmware
34+
$ python3 jpg2hex.py k8x8.jpg > jpg.hex
35+
```
36+
37+
You can replace `k8x8.jpg` with some other JPEG file to try with that. Note that the system has an overall memory limitation so any file larger than about 100x100 will most likely run into problems.
38+
39+
### Step 2.1 - Build and run with iverilog
40+
```sh
41+
$ make
42+
```
43+
Just typing the above command (while you are in the `nanojpeg` folder, not inside one of the subfolders) will take care of compiling and running with iverilog.
44+
45+
**WARNING**: This is *horrendously* slow - it takes about 6-7 *minutes* to run on the default input file, which is just a single JPEG macroblock and the entire image is of size 8 pixels by 7 pixels.
46+
47+
Therefore if you try this with another file (say `kitten.jpg`, which is 24x22 macroblocks in size), you can expect it to take more than 3000 minutes -- that is, more than 2 days to run. Please do not try this on the EE2003 server - if the system shows excessive load it will be restarted more than once a day as needed, so simulations will almost certainly not run to completion.
48+
49+
### Step 2.2 - Build and run with verilator
50+
Fortunately, there is a *much* faster verilog simulator called `verilator`. This works by first converting the Verilog code into C++, compiling it, and running the resulting executable. This can actually finish simulating the entire `kitten.jpg` input in less than 1 minute. If you want to test any changes to your code, you are strongly advised to use this approach.
51+
52+
To run this, you can just type
53+
```sh
54+
$ make test_verilator
55+
```
56+
This is already set up to take the exact same inputs and generate the same output.
57+
58+
### Step 3 - understand the results
59+
When you run the code, you will see that it generates a file named `output.dump` in the main `nanojpeg` folder. You can rename this file as `output.ppm`, and then it should be possible to view this file. Note that you cannot view it on the server, you will need to download the file to your local machine and then view it.
60+
61+
The default input will generate an image of a kitten that is 8x7 pixels in size -- in other words, if you recognize it as a kitten, you have a very good imagination. Instead, the actual output generated by running the converter on another PC is also available in the file `firmware/k8x8.ppm`.
62+
63+
Note that there is currently a bug in the code that results in one extra byte being added to the output. This means that you cannot directly compare the two files to check for correctness. However, if you use the command
64+
```sh
65+
$ xxd output.dump
66+
```
67+
it will dump out the hex formatted output, and here you can see that it matches the original except for the last byte.
68+
69+
## What to do next
70+
The present code not only runs the decoder, it also prints out the total number of clock cycles taken for the code to run. The fact that it is slow in simulation is not relevant, so do not try to speed that up. Instead, your goal should be to identify where the bottleneck is in the existing code, and to see if there are parts of it that can be accelerated in hardware.
71+
72+
Converting the entire code to hardware is not just difficult, it is also probably a bad idea, since there is a lot of conditional logic which is well suited to software, but will result in very bad and irregularly structured hardware.
73+
74+
On the other hand, certain blocks (in particular the DCT computations) are well suited to hardware implementation. So if you can create a module (similar to the seq_mult in assignment 1) that takes in appropriate inputs, does the computations in hardware and returns the results, there is a chance that you may speed things up.
75+
76+
Why only a chance? Because the transfer of data into and out of the hardware module will require the CPU to do read/write or load/store operations. It is quite possible that depending on how this is done, it may end up taking longer in hardware than to just do the computations in software.
77+
78+
You will need to *instrument* your code to find out where your changes will be most effective - that is, put in appropriate `get_num_cycles` calls in places where you can trace the time taken.
79+
80+
## Alternative ideas
81+
This is, as explained at the beginning, just a *starter* project. You are welcome to build on this in any way you want, or even completely ignore it and do another project of your own. For such projects, you are of course expected to run the ideas past the course instructor to ensure that your project idea is feasible and reasonable for the requirements of the course.

0 commit comments

Comments
 (0)