Experimenting with the BeagleV-Fire
My experience getting started with a BeagleV-Fire SoC (RiscV + FPGA) SBC.
Topics Covered
- My experience getting started with the BeagleV-Fire SoC development board.
Background
I have been reading a lot about RISC-V over the last year and have been wanting to experiment with some of the many popular development boards. When I came across an article about the PolarFire System on Chip (SoC) design from Microchip I was instantly intrigued about the possibilities of combining the RISC-V with on chip FPGA resources. So I ordered a new BeagleV-Fire development board to tinker with it and try out some cross-platform development.
My Journey
Boot-up and Go
I started this experiment on a fresh installation of Ubuntu 22.04 which I then configured with my development preferences and utilities.
The BeagleV-Fire comes with a version of Ubuntu installed on the eMMC out of the box. It was able to connect it to the LAN, start the board up, and ssh into it in a few minutes with no problems. It even came with an older version of Rust pre-installed which was a nice surprise. I ran into a minor issue when trying to update Rust to the newest version because while cargo
and rustc
are installed, rustup
is not. The eMMC is only 16 Gb so I can understand not wanting to include the package manager with the core functionalities. I went to update the Rust components but received an error because rustup
detected an existing installation. I removed rustc
with the package manager and the normal rustup installation proceeded without issue.
sudo apt remove rustc
With the latest version of Rust installed, I could created a new cargo package with a “Hello World” message and everything worked fine.
Imaging the SoC
The MPFS250T (PolarFire SoC) is a little different than other devices I have worked with in that the eMMC and microSD port are multiplexed in the MPFS250T so that only one or the other can be accessed by the chip directly. The BeagleV-Fire designers chose the eMMC route for booting and OS storage and exposed the microSD port through the OS via a SPI-MMC device in the device tree. For bulk data storage I inserted a formatted microSD card and searched for it with a lsblk
command where it showed up as mmcblk1
.
beagle@beaglev:~$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
mmcblk0 179:0 0 14.6G 0 disk
├─mmcblk0p1 179:1 0 684K 0 part
├─mmcblk0p2 179:2 0 60M 0 part /boot/firmware
└─mmcblk0p3 179:3 0 14.5G 0 part /
mmcblk1 179:8 0 119.1G 0 disk
└─mmcblk1p1 179:9 0 119.1G 0 part
mmcblk0boot0 179:16 0 4M 1 disk
mmcblk0boot1 179:24 0 4M 1 disk
Unfortunately, after trying everything could google could throw at me, I couldn’t get the drive to mount properly for use as additional storage space. The discord support forum had several posts that the support for the sd card wasn’t ready yet so maybe this will be working soon.
To access the eMMC for flashing a new OS image, the multiplexer needs to be switched to expose the eMMC storage through the USB-C port. This is done with a serial RS-232 connection on the debug header. I setup a Raspberry Pi Debug probe and hooked Rx, Tx, and Gnd up according to the instructions and opened a serial terminal with Tio. Rebooting produces a progress bar and right as it completes there is a prompt to hit any key alphanumeric key (space doesn’t work). When I timed it right, the HSS command prompt showed up and I could switch the eMMC to the USB-C. My Ubuntu installation detected it immediately and mounted it without issues. Flashing was straightforward with the following highlights:
- The most recent images can be found in this repository.
- The documentation implied that would find a
*.img
file but I didn’t find one in the download. - After poking around a bit I figured out that Balena etcher would accept the
sdcard.img.xz
file so that is what I used. - After flashing, the first boot took a bit longer and appeared to hang after a verification step of some kind. This might or might not be intentional. Resetting the board proceeded normally on the second boot.
Software Installation
The PolarFire SoC requires Microchip’s Libero Design Suite. When installing the suite there are a few gotchas that can derail the process and make it much more painful that necessary. I recommend following the instructions here as they mostly cover those gotchas. I will highlight what I learned from my experience.
- Install the Linux Standard Base (lsb) package. This is required for the licensing daemons or they output a misleading error that will lead down a rabbit hole.
- Create a directory to contain all the Microchip software. Something like
~/Microchip
will work well and will prevent manual modification of helpful script files. - Do not install the Microchip software in the default location.
- Do install Libero, SoftConsole, Licenses, etc in your
~/Microchip
directory. - Read the post-install instructions after installing SoftConsole. If that step is missed, the documentation can be found after the install at “~/Microchip/SoftConsole-v2022.2-RISC-V-747/documentation/softconsole/using_softconsole/post_installationhtml”
- Take advantage of the helpful script provided in the Microchip FPGA Tools Setup repo. Make sure to update it with your installation specific path details. It will then start the licensing daemons for you and configure the correct environment variables.
FPGA Programming (Defaults)
The documentation advises against trying to create FPGA logic from a blank Libero project. Instead the Gateware repository provides helpful scripts that create a pre-configured Libero project for you that can be customized to your needs.
Before I create custom FPGA logic, it makes sense to build and test a default bit stream to learn the process. Even though I probably don’t need local copies of these repositories, having them does make searching for documentation and figuring out how things go together much easier. I start by cloning them into a top level folder and end up with a tree like this:
./bbv_fire/
├── beaglev-fire
├── beaglev-fire-ubuntu
├── gateware
├── gateware-snapshots
├── hart-software-services
└── microchip-fpga-tools-setup
Next, I installed the Python 3 dependencies and tried to run the default build script.
python3 build-bitstream.py ./build-options/default.yaml
The build-bitstream.py
script worked well and provided useful output and guidance for problems. In my case, the setup-microchip-tools.sh
script that I modified to point to my installation directory contained an invalid path so I had to double-check my PATH
exports and compare them with the actual install paths. Once I had correct PATH
variables, the build completed without any issues.
Performing a git status on the repo shows that the build process has generated a few new folders. The gateware/work/
contains a pre-compiled HSS bootloader binary in addition to the script generated Libero project that executed the FPGA synthesis, routing, etc to generate the bitstream. The gateware/bitstream
folder contains the actual bitstream output I am most interested in.
Now the documentation didn’t provide a large amount of detail as to how to actually load the FPGA image but it does point to a script to execute on the BeagleV device at /usr/share/beagleboard/gateware
. I opened up that script and determined that it does the following things:
- Checks for sudo privileges.
- Checks the first argument to see if it is a valid directory.
- Checks for the presence of both
LinuxProgramming/mpfs_bitstream.spi
andLinuxProgramming/mpfs_dtbo.spi
. - If both checks succeed, calls the
update-gateware.sh
script in/usr/share/microchip/gateware
to do the heavy lifting.
So I used scp
to copy my generated LinuxProgramming
directory over to the home directory on the BeagleV device and ran the script:
beagle@BeagleV:/usr/share/beagleboard/gateware$ sudo ./change-gateware.sh /home/beagle/
The BeagleV device took a few minutes to finish flashing and rebooting and everything worked fine. It did feel strange to me to provide the parent of the LinuxProgramming
folder to the script so I may modify that so that it uses that folder directly.
Checking the serial console output from the reboot shows that the Design Name
is the same as it was before the flashing process. This might be expected since I built the default configuration so I will check this again in the next step when I build custom FPGA logic.
[5.779029] Design Info:
Design Name: CI_DEFAULT_FD28A2CA1789CDC1137
Design Version: 02.00.2
FPGA Programming (Custom)
Now that I know more about how the gateware creation process works I want to “unwind the stack” a bit and see how I can customize my FPGA. I will note that the documentation, while very helpful where it exists, is often missing. Be prepared to dive into the scripts to see what they do. The build-bitstream.py
is definitely worth a read and is almost better than a readme file.
The build-bitstream.py
needs a yaml
configuration file so I did a quick search and compared between the default.yaml
and the others to see how they work. They all had the HSS
object in common and I don’t want to modify that so it is safe to assume I can copy that section for my custom build. The also define a gateware
datatype which is what I want to customize so I again compared them all. They shared a type: sources
and listed various build-args
. Looking through the build-bitstream.py
helped me to understand this better and it turns out that each top level object is a key and each one is parsed to check it’s type
. Depending on the key.type
different actions are performed. For type: git
, the repository is cloned into the gateware/sources/
folder and *.zip
files are extracted there. If type: sources
no action is taken and the directory is added to the list of sources.
HSS:
type: git
link: https://git.beagleboard.org/beaglev-fire/hart-software-services.git
branch: develop-beaglev-fire
board: bvf
gateware:
type: sources
build-args: "M2_OPTION:NONE CAPE_OPTION:VERILOG_TUTORIAL"
unique-design-version: 9.0.2
Since no action is taken if type: sources
, the gateware/sources
folder seemed like a good place to start looking for more information. The FPGA-Design
sub-folder does have additional documentation on how to create a FPGA Libero project from a tickle script. The Readme.md
file provides an example command and documentation on the various options available. Running the command will trigger Libero to build a FPGA project through *.tcl
scripts using the provided options for configuration.
libero SCRIPT:BUILD_BVF_GATEWARE.tcl "SCRIPT_ARGS: ONLY_CREATE_DESIGN M2_OPTION:NONE CAPE_OPTION:NONE"
Reading through the available SCRIPT_ARGS
I notice that the CAPE_OPTION
provides additional details on how to customize my FPGA:
… Valid values are the directory names in ./script_support/components/CAPE. If you wish to create an alternate build option, add a new directory in ./script_support/components/CAPE using one of the existing ones as template. This is a good place to start if you want to play with FPGA digital logic.
Perfect! Now I need to figure out which one to use as a template to start from. Again I do a quick search and find that there is a Readme.md
file in most of the FPGA_design/script_support/components/CAPE/*
sub-folders that contain pin mapping documentation. I can compare these file in vscode or use a diff
command to see what changes are made from the default. I remembered noticing the gateware/custom-fpga-design/my_custom_fpga_design.yaml
configuration file in a earlier step and one of the build-args
was VERILOG_TUTORIAL
which seemed to bea good starting point for custom logic. So I start to dig around a bit more and find that in FPGA_design/script_support/components/CAPE/
there is both a VERILOG_TUTORIAL
and VERILOG_TEMPLATE
folder. Either of which should be viable for cloning and customizing. Unfortunately, I didn’t see a Readme.md
file for either of these capes but they both have a HDL
sub-folder that includes Verilog text files. It was pretty easy to view these in vscode to see and compare them the defaults.
For experimentation I decided to start with a clone of the VERILOG_TEMPLATE
folder and see if I could modify it as I would with custom logic so that it produces the same output as the VERILOG_TUTORIAL
.
$ cp -r ./VERILOG_TEMPLATE ./CUSTOM_CAPE
Once the template is copied, I open the new folder up in vscode and perform a search for VERILOG_TEMPLATE
and replace it with my new name CUSTOM_CAPE
. In this case, it only affected the the ADD_CAPE.tcl
script. Next I needed to create the Libero project so I can write, simulate, and synthesize my custom design.
aven:~/bbv_fire/gateware/sources/FPGA-design$ libero SCRIPT:BUILD_BVF_GATEWARE.tcl "SCRIPT_ARGS: ONLY_CREATE_DESIGN PROJECT_LOCATION:\"~/repos/riscv-fpga/my_custom_cape\" TOP_LEVEL_NAME:\"my_custom_cape\" M2_OPTION:NONE CAPE_OPTION:CUSTOM_CAPE"
Once the script is complete, I can open Libero and then open the newly created project in the folder I specified in the script. From here, I created a new HDL source file with the Create HDL
wizard and named it my_logic.v
. I am only interested in “simulating” the actual FPGA logic design process for now so instead of creating HDL from scratch, I copied the logic from VERILOG_TUTORIAL/HDL/blinky.v
and updated the CAPE.v
file to connect the wires. I double-checked the work with a diff
command on my HDL folder and the tutorial to make sure I updated everything right. Then I executed the design flow and went through synthesis, place & route, etc. without any issues.
There were several hundred warnings generated about “User defined pragma syn_black_box” declarations but from what I could tell, were possibly due to weird formatting of the code generated polarfire_syn_comps.v
file. They didn’t seem to have any real impact on my custom logic.
The next step was to copy my newly created and tested HDL files back into my cloned cape. Note that the script that generated the project also creates extra HDL files that weren’t in the original CUSTOM_CAPE/HDL
folder. When I run the build-bitstream.py
script these should get re-generated so I don’t think they need to be copied. I did copy them over anyway to see what would happen and it didn’t seem to break anything. Checking the output work
directory didn’t indicate any problems. These files include:
- miv_*.v
- apb_arbiter.v
- AXI4_address_shim.v
Now I am ready to create my own custom_cape.yaml
file from a copy of gateware/custom-fpga-design/my_custom_fpga_design.yaml
. I update the build-args
with the options that I want then take care to set unique-design-version
to something new. There are a few things to note here:
- The FPGA bitstream loading application will skip the flashing process if the
Design Version
on the FPGA matches what is already stored on the FPGA. So this is something that will need to be updated every build. - The
build-bitstream.py
script will look for “custom-fpga-design” in the path, and if the*.yaml
file is stored there it will specify the design version based on a hash of the git repo. In practical terms, this means that if mycustom_cape.yaml
is located in a folder named “custom-fpga-design” the design name and version will be the same from build to build and the FPGA loading operation will be skipped. I can work around this if I place my configuration file in a different folder or by making a commit to thegateware
repository between builds.
# gateware/custom_cape.yaml
HSS:
type: git
link: https://git.beagleboard.org/beaglev-fire/hart-software-services.git
branch: develop-beaglev-fire
board: bvf
gateware:
type: sources
build-args: "TOP_LEVEL_NAME:\"my_custom_cape\" M2_OPTION:NONE CAPE_OPTION:CUSTOM_CAPE"
unique-design-version: 0.0.1
With those changes in place I can kick off a build…
gateware$ python3 build-bitstream.py ./custom_cape.yaml
When the build completes, I check the gateware/bitstream/LinuxProgramming
folder and see I have newly created bitfiles. Just like before I use SCP to copy the LinuxProgramming
folder over to the home directory on the BeagleV and execute the change-gateware.sh
script. The serial output from the BeagleV then shows I have the correct design name and version installed.
[2.09806] Design Info:
Design Name: CUSTOM_CAPE_A5E630863F21B808BF
Design Version: 00.00.2
I then take a look at the board and see user LED 5 flashing on and off like I would expect! I took a scope to the output to check the timing and noticed that when the pin is in a “High” state it is actually super noisy around 1.5 volts. I surmise there is a problem with the logic that is causing the pin to turn on and off faster than my scope can measure. I will have to investigate further when I make part 2.
Next Steps
- The logic doesn’t appear to be working quite right so I need to investigate that and figure out what is going on.
- I would like to experiment with the peripherals such as I2C, SPI, and PWM.
- The workflow I have so far is functional but has room for improvement. I would like to have my FPGA design in a standalone project within it’s own repository so I am thinking I could do the following:
- Start off with newly created
*.yaml
file with a newkey
for the high level details of a new project such as PATH and cape to copy. - Create my own python script that clones the
VERILOG_TEMPLATE
cape and updates the tickle script automatically with the new name. - Then further update that script so that it creates a standalone project from the same
*.yaml
file that I use for programming the device. - I could then modify the existing python scripts to copy the HDL folder from a
type: external_source
.
- Start off with newly created