Building FPGAs with Quartus+CMake
Quartus Prime has an IDE interface for building FPGA projects which can work well, but for me it falls short in a few areas. We’ll look at how we can integrate Quartus FPGA building in CMake so it can be built along with the rest of a project.
The main issues with using Quartus to develop an FPGA design for me are:
- I’m fast and comfortable in vim, I’d rather use my own editor.
- Quartus builds the project in-place which can make it troublesome to have multiple configurations building at the same time.
- Building requires a different workflow to building Verilator simulations.
- Hooking other build steps such as microcode ROMs requires some hacky TCL.
- There ends up being duplication of lists source files and other resources leading to a maintenance burden.
In the previous post we looked at how to install Quartus Prime into Docker, and now we’ll look at how we can add CMake into the mix to address some of these issues.
Finding Quartus
The first thing we need to do is to teach CMake how to find Quartus. If you’ve followed the previous post and are running your builds in Docker, the following will work, otherwise you might need to add some additional hints.
include(FindPackageHandleStandardArgs)
set(QUARTUS_PATHS
/opt/altera/quartus/bin/
/opt/altera/16.1/quartus/bin/
${HOME}/altera/16.1/quartus/bin)
find_program(QUARTUS_SH_EXECUTABLE NAMES quartus_sh
PATHS ${QUARTUS_PATHS})
find_program(QUARTUS_MAP_EXECUTABLE NAMES quartus_map
PATHS ${QUARTUS_PATHS})
find_program(QUARTUS_FIT_EXECUTABLE NAMES quartus_fit
PATHS ${QUARTUS_PATHS})
find_program(QUARTUS_ASM_EXECUTABLE NAMES quartus_asm
PATHS ${QUARTUS_PATHS})
find_program(QUARTUS_STA_EXECUTABLE NAMES quartus_sta
PATHS ${QUARTUS_PATHS})
find_program(QUARTUS_PGM_EXECUTABLE NAMES quartus_pgm
PATHS ${QUARTUS_PATHS})
find_package_handle_standard_args(Quartus FOUND_VAR QUARTUS_FOUND
REQUIRED_VARS
QUARTUS_SH_EXECUTABLE
QUARTUS_MAP_EXECUTABLE
QUARTUS_FIT_EXECUTABLE
QUARTUS_ASM_EXECUTABLE
QUARTUS_STA_EXECUTABLE
QUARTUS_PGM_EXECUTABLE)
This tells CMake to look for the programs we need in a couple of locations and
then set some variables as appropriate. Put this file in your CMake library
search path (such as CMake/FindQuartus.cmake
), and in your project
CMakeLists.txt
, tell CMake to find Quartus:
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/CMake/")
find_package(Quartus REQUIRED)
Invoking Quartus
Now that CMake can find the Quartus tools, let’s teach it how to put together an FPGA.
In a new file CMake/Quartus.cmake
, add the following:
function(add_fpga)
set(options "")
set(oneValueArgs PROJECT FAMILY PART)
set(multiValueArgs SOURCES DEPENDS)
cmake_parse_arguments(add_fpga "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
foreach(source ${add_fpga_SOURCES})
list(APPEND SOURCE_ARGS --source=${source})
endforeach(source)
add_custom_command(OUTPUT ${add_fpga_PROJECT}.qpf
COMMAND ${QUARTUS_SH_EXECUTABLE} --prepare -f ${add_fpga_FAMILY} -t ${add_fpga_PROJECT} ${add_fpga_PROJECT}
DEPENDS ${add_fpga_PROJECT}.qsf)
add_custom_command(OUTPUT ${add_fpga_PROJECT}.map.rpt
COMMAND ${QUARTUS_MAP_EXECUTABLE} ${SOURCE_ARGS} --family ${add_fpga_FAMILY} --optimize=speed ${add_fpga_PROJECT}
DEPENDS ${add_fpga_DEPENDS} ${add_fpga_SOURCES} ${add_fpga_PROJECT}.qpf ${add_fpga_PROJECT}.qsf)
add_custom_command(OUTPUT ${add_fpga_PROJECT}.fit.rpt
COMMAND ${QUARTUS_FIT_EXECUTABLE} --part=${add_fpga_PART} --read_settings_file=on --set=SDC_FILE=${add_fpga_PROJECT}.sdc ${add_fpga_PROJECT}
DEPENDS ${add_fpga_PROJECT}.map.rpt)
add_custom_command(OUTPUT ${add_fpga_PROJECT}.asm.rpt
COMMAND ${QUARTUS_ASM_EXECUTABLE} ${add_fpga_PROJECT}
DEPENDS ${add_fpga_PROJECT}.fit.rpt)
add_custom_command(OUTPUT ${add_fpga_PROJECT}.sta.rpt ${add_fpga_PROJECT}.sof
COMMAND ${QUARTUS_STA_EXECUTABLE} ${add_fpga_PROJECT}
DEPENDS ${add_fpga_PROJECT}.asm.rpt)
endfunction()
Putting it together
Now let’s put all of this together and actually build something. In your
CMakeLists.txt
add something like:
include(Quartus)
configure_file(Top.qsf Top.qsf)
configure_file(Top.srf Top.srf)
configure_file(Top.sdc Top.sdc)
set(FPGA_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/Top.sv)
add_fpga(PROJECT Top
FAMILY "Cyclone IV E"
PART "EP4CE22F17C6"
SOURCES ${FPGA_SOURCES})
add_custom_command(OUTPUT __program_de0_nano__
COMMAND ${QUARTUS_PGM_EXECUTABLE} --mode=jtag -o \"P\;Top.sof\"
DEPENDS Top.sof)
add_custom_target(de0-nano DEPENDS Top.sta.rpt)
add_custom_target(de0-nano-program DEPENDS __program_de0_nano__)
Then create the Quartus settings file (Top.qsf
), the suppression rules file
(Top.srf
which can start off empty) and the Synopsys timing constraints file
(Top.sdc
). Now we can use add_fpga()
to build an FPGA with the given part
and sources. I’ve also added a rule to program the board for development
purposes.
Note that in this case, I haven’t added the FPGA to the default target - compared to a Verilator simulation, Quartus takes a long time to build a design, so I’d rather only build that once I’ve built my simulations and run all tests against those. Once everything is hooked up, we’re ready to build:
mkdir -p _build
cd _build
cmake ..
make de0-nano
make de0-nano-program
and that’s it - your FPGA build is a first class citizen, just like any of your other applications/libraries in your project. Additional dependencies can be passed to add_fpga() so you can use generated files in your build.
See a [https://github.com/jamieiles/fpga-cmake](sample project) using this to build a simple FPGA design.