Thursday, February 14, 2013

More BGA process analysis

I was recently asked by an online acquaintance to review his BGA soldering process and make suggestions to improve yield.

The test board was made on OSHPark's purple batch service and featured a large 784-ball BGA footprint as well as two DDR2 footprints and some 0402 pads.

Test board
The boards I actually received had a FT256 packaged device on the 784-ball footprint since he couldn't find any cheap 784-ball devices.

The analysis being performed was a "dye and pry", a destructive test of joint quality. Step 1 is to squirt a dye of some sort under the BGA and bake to remove the solvent.

Dying the chips
I didn't have any red machinist's layout fluid (the normal choice in professional shops) so I made something of my own by performing a solvent extraction of a fluorescent yellow highlighter in IPA.

Fully dyed board
The next step was to bake the board in an oven until all of the solvent had evpaorated. I didn't bake long enough so some of the dye was still liquid when I did the "pry" operation, making results for some of the outer balls questionable.

Once the dye has dried the next step is the "pry" operation: insert a small screwdriver under the chip and pry up around the edge until it pops off. By looking at where the dye reached, cracks in the joints can be seen.

I imaged balls of concern with a 10x objective in epi-illumination darkfield mode, then stacked the image with a brightfield image take under 385nm UV illumination for fluorescence.

BGA ball with large void
No cracks were visible but several balls had fairly large voids. The one pictured above extended to the edge of the ball and covered 25.9% of the ball area, which fails the IPC 7095 class I standard (25% of ball area) for acceptable voiding at the package-ball interface. The majority of balls were within class I tolerances and most met the more stringent class II requirements as well, suggesting that while his process does have unacceptable voiding, not much improvement is necessary.

This got me curious as to how much voiding was present in my own boards. I ran the same test on an XC3S50A-4FTG256 on a dummy board using my standard process.

BGA ball with 6.2% voiding
The first ball I looked at had 6.2% voiding, well within the class II requirement (12.25%) and failing class III (4% voiding).

Lacking machine-vision tools to rapidly inspect all of the balls I decided to do a manual worst-case analysis and find the ball with the most visible voids.

BGA ball with 22.4% voiding
I measured the worst ball at 22.4% voiding, slightly within the class I limit. While my process is still far from ideal, class I (normally used for typical consumer electronics) is more than acceptable for hobbyist prototypes.

Friday, January 11, 2013

New decap setup

I finally got an IC decapsulation setup up and running in my new lab. Been a while but I'm back in business :)

Now that I have a proper (homebrewed) fume hood decapping is a lot easier and safer. No fumes to breathe, and a nice thick sash between me and the beaker of hot acid if something goes wrong. That said, I still wear goggles, gloves, and a lab coat as per SOP.

For the new process I am using sulfuric acid as it's much cheaper than nitric. (Concentrated sulfuric of an acceptable quality can be purchased as a drain cleaner, while nitric must be mail-ordered with special hazmat shipping fees). The downside is that sulfuric cannot be used for live decapsulation (at least not easily) since it takes so long. For bare-die decaps it's by far the most cost-effective option, though.

The first step, as is usual for bare-die decaps, is to put them in a beaker of acid, cover with a watch glass or petri dish to slow down evaporation losses. and heat.

View through the sash of my fume hood as the acid warms up
Pure sulfuric acid starts out a pale yellowish to clear but turns reddish-brown and then black from carbon particles as the decap proceeds. Drain-cleaner grade will normally be stored in a plastic bottle and have absorbed some organics already, causing discoloration.

Close up of beaker on the hot plate
As the decap progresses the beaker fills up with acid fumes and the acid turns darker.

Later in the decap

At this point all that has to be done is to leave the chips to cook for a while. I was running at only around 100C (150-200 is more common) so it took an hour or two.

Before I get to the results, a warning as to what you're dealing with here. While pouring the used acid off into another beaker to pull the dies out with tweezers, I spilled a drop on a kimwipe I had in the hood. This was the result:

H2SO4 + organics = bad
In this picture I was decapping a pair of FTDI FT232RL USB-serial adapters as well as a MAX232. The MAX232 wasn't left in quite long enough so I'm going to need to clean it with more acid later on. Here's a pic of the FT232R:

Top metal of FT232RL
As usual I uploaded all of my images to the Silicon Archive:

Wiki page: http://siliconpr0n.org/archive/doku.php?id=ftdi_ft232rl

Monday, December 3, 2012

CMake, CTest, and CDash for Xilinx FPGAs, part 2

This is a follow-up to my post from yesterday. I've made major progress and, if I knew things would go this fast, wouldn't have written that post until today :)

The current version of the script is able to compile HDL designs to both FPGA bitstreams and ISim test cases, as well as running the simulation executable in the form of a unit test. There's no direct support for CPLDs yet (which will pretty much involve refactoring the code to call xst out into a function and adding some code to call cpldfit) but that will come soon.

Also on the to-do list:
  • Support for invoking PlanAhead in both pre-synthesis and post-PAR modes
  • Support for programming bitstreams to FPGAs and CPLDs using iMPACT via a "make program" type target
  • Support for indirect programming (need to generate ROM files etc)
  • Support for programming bitstreams to FPGAs and CPLDs using my JTAG toolchain (uses libftdi and the Digilent API as back ends, so I can integrate FT2232-based debug/program modules into my boards and not rely on the Xilinx platform cable)
  • Support for more command-line flags for the toolchain. Right now all of the ngdbuild/map/par/trce/bitgen flags are hard-coded and only about half of the default xst flags are changeable.
  • Support for mixed hardware/ISim/C++ cosimulation (using pipes and $fread/$fwrite to bridge to ISim and JTAG to bridge to real hardware)
Without further ado, here's a usage example for the major new feature:

########################################################################################################################
# Global synthesis flags

set(XILINX_FILTER_FILE ${CMAKE_CURRENT_SOURCE_DIR}/filter.filter)

set(XST_KEEP_HIERARCHY Soft)
set(XST_NETLIST_HIERARCHY Rebuilt)

########################################################################################################################
# Current top-level module
add_fpga_target(
 OUTPUT
  JtagTest
 TOP_LEVEL
  ${CMAKE_CURRENT_SOURCE_DIR}/JtagTest.v
 CONSTRAINTS
  ${CMAKE_SOURCE_DIR}/ucf/JtagTest.ucf
 DEVICE
  xc6slx45-3-csg324
 SOURCES
  ${CMAKE_CURRENT_SOURCE_DIR}/debug/JtagDebugController.v
  ${CMAKE_CURRENT_SOURCE_DIR}/noc/common/NOCArbiter.v
  ${CMAKE_CURRENT_SOURCE_DIR}/noc/common/NOCRouterCore.v
  ${CMAKE_CURRENT_SOURCE_DIR}/noc/common/NOCRouterMux.v
  ${CMAKE_CURRENT_SOURCE_DIR}/noc/rpc/RPCRouter.v
  ${CMAKE_CURRENT_SOURCE_DIR}/noc/rpc/RPCRouterExitQueue.v
  ${CMAKE_CURRENT_SOURCE_DIR}/peripherals/NetworkedButtonArray.v
  ${CMAKE_CURRENT_SOURCE_DIR}/peripherals/NetworkedLEDBank.v
  ${CMAKE_CURRENT_SOURCE_DIR}/util/MediumBlockRamFifo.v
  ${CMAKE_CURRENT_SOURCE_DIR}/util/SwitchDebouncer.v
  ${CMAKE_CURRENT_SOURCE_DIR}/util/SwitchDebouncerBlock.v
  ${CMAKE_CURRENT_SOURCE_DIR}/util/ThreeStageSynchronizer.v
 )

The add_fpga_target function uses the OUTPUT parameter as the base name for all of the temporary files created during compilation.

The TOP_LEVEL parameter specifies the top-level source file for the module. For now the base name of the TOP_LEVEL file is used as the top-level module name; in the future I may make the TOP_LEVEL parameter specify the module name and then add that file (along with all the others) to the SOURCES section.

DEVICE and SOURCES should be self-explanatory. Note that the Xilinx toolchain expects the part numbers in a specific format - there's a dash between the speed grade and the package (unlike the actual part numbers) and the temperature range is not specified.

Full source for this monster is below. Now that it's reached the point of basic usability I won't be blogging on it anymore except to announce the stable release on Google Code once I've worked out the rest of the kinks and bugs.

########################################################################################################################
# @file FindXilinx.cmake
# @author Andrew D. Zonenberg
# @brief Xilinx ISE toolchain CMake module
########################################################################################################################

########################################################################################################################
# Autodetect Xilinx paths (very hacky for now)

# TODO: Print messages only when configuring

# Find /opt/Xilinx or similar
find_file(XILINX_PARENT NAMES Xilinx PATHS /opt)
if(XILINX_PARENT STREQUAL "XILINX_PARENT-NOTFOUND")
 message(FATAL_ERROR "No Xilinx toolchain installation found")
endif()

# Find /opt/Xilinx/VERSION
# TODO: Figure out a better way of doing this
find_file(XILINX NAMES 14.3 PATHS ${XILINX_PARENT})
if(XILINX STREQUAL "XILINX-NOTFOUND")
 message(FATAL_ERROR "No ISE 14.3 installation found")
endif()
#message(STATUS "Found Xilinx toolchain... ${XILINX}")

# Set current OS architecture (TODO: autodetect)
set(XILINX_ARCH lin64)

# Find fuse
find_program(FUSE names fuse PATHS "${XILINX}/ISE_DS/ISE/bin/${XILINX_ARCH}/" NO_DEFAULT_PATH)
if(FUSE STREQUAL "FUSE-NOTFOUND")
 message(FATAL_ERROR "No Xilinx fuse installation found")
endif()
#message(STATUS "Found Xilinx fuse... ${FUSE}")

# Find xst
find_file(XST NAMES xst PATHS "${XILINX}/ISE_DS/ISE/bin/${XILINX_ARCH}/")
if(XST STREQUAL "XST-NOTFOUND")
 message(FATAL_ERROR "No Xilinx xst installation found")
endif()
#message(STATUS "Found Xilinx xst... ${XST}")

# Find ngdbuild
find_file(NGDBUILD NAMES ngdbuild PATHS "${XILINX}/ISE_DS/ISE/bin/${XILINX_ARCH}/")
if(NGDBUILD STREQUAL "NGDBUILD-NOTFOUND")
 message(FATAL_ERROR "No Xilinx ngdbuild installation found")
endif()
#message(STATUS "Found Xilinx ngdbuild... ${NGDBUILD}")

# Find map
find_file(MAP NAMES map PATHS "${XILINX}/ISE_DS/ISE/bin/${XILINX_ARCH}/")
if(MAP STREQUAL "MAP-NOTFOUND")
 message(FATAL_ERROR "No Xilinx map installation found")
endif()
#message(STATUS "Found Xilinx map... ${MAP}")

# Find par
find_file(PAR NAMES par PATHS "${XILINX}/ISE_DS/ISE/bin/${XILINX_ARCH}/")
if(PAR STREQUAL "PAR-NOTFOUND")
 message(FATAL_ERROR "No Xilinx par installation found")
endif()
#message(STATUS "Found Xilinx par... ${PAR}")

# Find trce
find_file(TRCE NAMES trce PATHS "${XILINX}/ISE_DS/ISE/bin/${XILINX_ARCH}/")
if(TRCE STREQUAL "TRCE-NOTFOUND")
 message(FATAL_ERROR "No Xilinx trce installation found")
endif()
#message(STATUS "Found Xilinx trce... ${TRCE}")

# Find bitgen
find_file(BITGEN NAMES bitgen PATHS "${XILINX}/ISE_DS/ISE/bin/${XILINX_ARCH}/")
if(BITGEN STREQUAL "BITGEN-NOTFOUND")
 message(FATAL_ERROR "No Xilinx bitgen installation found")
endif()
#message(STATUS "Found Xilinx bitgen... ${BITGEN}")

########################################################################################################################
# Argument parsing helper

macro(xilinx_parse_args _output _top_level _ucf _device _sources)
 set(${_top_level} FALSE)
 set(${_output} FALSE)
 set(${_ucf} FALSE)
 set(${_device} FALSE)
 set(${_sources})
 set(_found_sources FALSE)
 set(_found_device FALSE)
 set(_found_output FALSE)
 set(_found_ucf FALSE)
 set(_found_top_level FALSE)
 foreach(arg ${ARGN})
  if(${arg} STREQUAL "TOP_LEVEL")
   set(_found_top_level TRUE)
  elseif(${arg} STREQUAL "SOURCES")
   set(_found_sources TRUE)
  elseif(${arg} STREQUAL "CONSTRAINTS")
   set(_found_ucf TRUE)
  elseif(${arg} STREQUAL "DEVICE")
   set(_found_device TRUE)
  elseif(${arg} STREQUAL "OUTPUT")
   set(_found_output TRUE)
  elseif(${_found_sources})
   list(APPEND ${_sources} ${arg})
  elseif(${_found_device})
   if(${_device})
    message(FATAL_ERROR "Multiple devices specified in xilinx_parse_args")
   else()
    set(${_device} ${arg})    
   endif()
  elseif(${_found_ucf})
   if(${_ucf})
    message(FATAL_ERROR "Multiple constraint files specified in xilinx_parse_args")
   else()
    set(${_ucf} ${arg})    
   endif()
  elseif(${_found_top_level})
   if(${_top_level})
    message(FATAL_ERROR "Multiple top-level files specified in xilinx_parse_args (${_top_level})")
   else()
    set(${_top_level} ${arg})    
   endif()
  elseif(${_found_output})
   if(${_output})
    message(FATAL_ERROR "Multiple outputs specified in xilinx_parse_args")
   else()
    set(${_output} ${arg})    
   endif()
  else()
   message(FATAL_ERROR "Unrecognized command ${arg} in xilinx_parse_args")
  endif()
 endforeach()
endmacro()

########################################################################################################################
# Default flags for fuse
set(FUSE_FLAGS "-intstyle ise -incremental -lib unisims_ver -lib unimacro_ver -lib xilinxcorelib_ver -lib secureip")

########################################################################################################################
# ISim executable generation

function(add_isim_executable OUTPUT_FILE )
  
 # Parse args
 xilinx_parse_args(OUTFNAME TOP_LEVEL UCF DEVICE SOURCES ${ARGN})
 
 # Get base name without extension of the top-level module
 get_filename_component(TOPLEVEL_BASENAME ${TOP_LEVEL} NAME_WE )
 
 # Write the .prj file
 set(PRJ_FILE "${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_FILE}.prj")
 file(WRITE ${PRJ_FILE} "verilog work \"${TOP_LEVEL}\"\n")
 foreach(f ${SOURCES})
  file(APPEND ${PRJ_FILE} "verilog work \"${f}\"\n")
 endforeach()
 file(APPEND ${PRJ_FILE} "verilog work \"${XILINX}/ISE_DS/ISE/verilog/src/glbl.v\"\n")
 
 # Write the run-fuse wrapper script
 set(FUSE_ERR_LOG "${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_FILE}_err.log")
 set(FUSE_LOG "${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_FILE}_build.log")
 set(FUSE_WRAPPER "${CMAKE_CURRENT_BINARY_DIR}/runfuse${OUTPUT_FILE}.sh")
 file(WRITE ${FUSE_WRAPPER} "#!/bin/bash\n")
 file(APPEND ${FUSE_WRAPPER} "cd ${CMAKE_CURRENT_BINARY_DIR}\n")
 #file(APPEND ${FUSE_WRAPPER} "source ${XILINX}/ISE_DS/settings64.sh\n")
 file(APPEND ${FUSE_WRAPPER} "${FUSE} ${FUSE_FLAGS} -o ${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_FILE} -prj ${PRJ_FILE}")
 file(APPEND ${FUSE_WRAPPER} "   work.${TOPLEVEL_BASENAME} work.glbl > ${FUSE_LOG} 2> ${FUSE_ERR_LOG}\n")
 file(APPEND ${FUSE_WRAPPER} "if [ \"$?\" != \"0\" ]; then\n")
 file(APPEND ${FUSE_WRAPPER} "    cat ${FUSE_ERR_LOG} | grep \"ERROR\"\n")
 file(APPEND ${FUSE_WRAPPER} "    exit 1;\n")
 file(APPEND ${FUSE_WRAPPER} "fi\n")
 file(APPEND ${FUSE_WRAPPER} "exit 0;\n")
 execute_process(COMMAND chmod +x ${FUSE_WRAPPER})
 
 # Main compile rule
 # TODO: tweak this
 add_custom_target(
  ${OUTPUT_FILE} ALL
  COMMAND ${FUSE_WRAPPER}
  DEPENDS ${SOURCES} ${TOP_LEVEL}
  COMMENT "Building ISim executable ${OUTPUT_FILE}..."
 )
 
 # Write the tcl script
 set(TCL_FILE "${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_FILE}.tcl")
 file(WRITE ${TCL_FILE} "onerror {resume}\n")
 file(APPEND ${TCL_FILE} "wave add /\n")
 file(APPEND ${TCL_FILE} "run 1000 ns;\n")
 file(APPEND ${TCL_FILE} "exit;\n")
 
 # Write the run-test wrapper script
 set(TEST_WRAPPER "${CMAKE_CURRENT_BINARY_DIR}/run${OUTPUT_FILE}.sh")
 file(WRITE ${TEST_WRAPPER} "#!/bin/bash\n")
 file(APPEND ${TEST_WRAPPER} "cd ${CMAKE_CURRENT_BINARY_DIR}\n")
 file(APPEND ${TEST_WRAPPER} "source ${XILINX}/ISE_DS/settings64.sh\n")
 file(APPEND ${TEST_WRAPPER} "./${OUTPUT_FILE} -tclbatch ${TCL_FILE} -intstyle silent -vcdfile ${OUTPUT_FILE}.vcd -vcdunit ps || exit 1\n")
 file(APPEND ${TEST_WRAPPER} "cat isim.log | grep -q FAIL\n")
 file(APPEND ${TEST_WRAPPER} "if [ \"$?\" != \"1\" ]; then\n")
 file(APPEND ${TEST_WRAPPER} "    exit 1;\n")
 file(APPEND ${TEST_WRAPPER} "fi\n")
 execute_process(COMMAND chmod +x ${TEST_WRAPPER})
 
endfunction()

########################################################################################################################
# Test generation
#
# Usage:
# add_isim_test(NandGate
# TOP_LEVEL
#  ${CMAKE_CURRENT_SOURCE_DIR}/testNandGate.v
# SOURCES 
#  ${CMAKE_SOURCE_DIR}/hdl/NandGate.v
# )

function(add_isim_test TEST_NAME)

 # Parse args
 xilinx_parse_args(OUTPUT TOP_LEVEL UCF DEVICE SOURCES ${ARGN})

 # Add the sim executable
 add_isim_executable(test${TEST_NAME}
  TOP_LEVEL
   ${TOP_LEVEL}
  SOURCES 
   ${SOURCES}
  )

 add_test(${TEST_NAME}
  "${CMAKE_CURRENT_BINARY_DIR}/runtest${TEST_NAME}.sh")
 set_property(TEST ${TEST_NAME} APPEND PROPERTY DEPENDS test${TEST_NAME})


endfunction()

########################################################################################################################
# Default flags for Xilinx toolchain

# Compiler flags
set(XST_MAX_FANOUT 100000)
set(XST_OPT_MODE Speed)
set(XST_OPT_LEVEL 1)
set(XST_KEEP_HIERARCHY No)
set(XST_NETLIST_HIERARCHY As_Optimized)
set(XST_RESOURCE_SHARING Yes)
set(XST_RAM_EXTRACT Yes)
set(XST_SHREG_MIN_SIZE 2)
set(XST_REGISTER_BALANCING No)

set(XILINX_FILTER_FILE FALSE)

########################################################################################################################
# Xilinx FPGA bitstream generation

function(add_fpga_target)
 
 # Parse args
 xilinx_parse_args(OUTFNAME TOP_LEVEL UCF DEVICE SOURCES ${ARGN})
 
 # Get base name without extension of the top-level module
 get_filename_component(TOPLEVEL_BASENAME ${TOP_LEVEL} NAME_WE )
 
 # Set the filter flag
 SET(XILINX_FILTER_FLAG "")
 if(XILINX_FILTER_FILE)
  SET(XILINX_FILTER_FLAG "-filter ${XILINX_FILTER_FILE}")
 ENDIF()
 
 # Write the .prj file
 set(PRJ_FILE "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}.prj")
 file(WRITE ${PRJ_FILE} "verilog work \"${TOP_LEVEL}\"\n")
 foreach(f ${SOURCES})
  file(APPEND ${PRJ_FILE} "verilog work \"${f}\"\n")
 endforeach()
 file(APPEND ${PRJ_FILE} "verilog work \"${XILINX}/ISE_DS/ISE/verilog/src/glbl.v\"\n")
 
 # Create the XST input script
 set(XST_DIR "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}_xst")
 file(MAKE_DIRECTORY ${XST_DIR})
 set(XST_TMPDIR "${XST_DIR}/projnav.tmp")
 file(MAKE_DIRECTORY ${XST_TMPDIR})
 set(XST_SCRIPT_FILE "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}.xst")
 set(XST_SYR_FILE "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}.syr")
 file(WRITE  ${XST_SCRIPT_FILE} "set -tmpdir \"${XST_TMPDIR}\"\n")
 file(APPEND ${XST_SCRIPT_FILE} "set -xsthdpdir ${XST_DIR}\n")
 file(APPEND ${XST_SCRIPT_FILE} "run\n")
 file(APPEND ${XST_SCRIPT_FILE} "-ifn ${PRJ_FILE}\n")
 file(APPEND ${XST_SCRIPT_FILE} "-ofn ${OUTFNAME}\n")
 file(APPEND ${XST_SCRIPT_FILE} "-ofmt NGC\n")
 file(APPEND ${XST_SCRIPT_FILE} "-p ${DEVICE}\n")
 file(APPEND ${XST_SCRIPT_FILE} "-top ${TOPLEVEL_BASENAME}\n")
 file(APPEND ${XST_SCRIPT_FILE} "-slice_utilization_ratio 100\n")
 file(APPEND ${XST_SCRIPT_FILE} "-bram_utilization_ratio 100\n")
 file(APPEND ${XST_SCRIPT_FILE} "-dsp_utilization_ratio 100\n")
 file(APPEND ${XST_SCRIPT_FILE} "-bufg 16\n")
 file(APPEND ${XST_SCRIPT_FILE} "-hierarchy_separator /\n")
 file(APPEND ${XST_SCRIPT_FILE} "-bus_delimiter <>\n")
 file(APPEND ${XST_SCRIPT_FILE} "-case Maintain\n")
 file(APPEND ${XST_SCRIPT_FILE} "-max_fanout ${XST_MAX_FANOUT}\n")
 file(APPEND ${XST_SCRIPT_FILE} "-opt_mode ${XST_OPT_MODE}\n")
 file(APPEND ${XST_SCRIPT_FILE} "-opt_level ${XST_OPT_LEVEL}\n")
 file(APPEND ${XST_SCRIPT_FILE} "-keep_hierarchy ${XST_KEEP_HIERARCHY}\n")
 file(APPEND ${XST_SCRIPT_FILE} "-netlist_hierarchy ${XST_NETLIST_HIERARCHY}\n")
 file(APPEND ${XST_SCRIPT_FILE} "-resource_sharing ${XST_RESOURCE_SHARING}\n")
 file(APPEND ${XST_SCRIPT_FILE} "-ram_extract ${XST_RAM_EXTRACT}\n")
 file(APPEND ${XST_SCRIPT_FILE} "-shreg_min_size ${XST_SHREG_MIN_SIZE}\n")
 file(APPEND ${XST_SCRIPT_FILE} "-register_balancing ${XST_REGISTER_BALANCING}\n")
 
 #-power NO
 #-iuc NO
 #-rtlview Yes
 #-glob_opt AllClockNets
 #-read_cores YES
 #-write_timing_constraints NO
 #-cross_clock_analysis NO
 #-lc Auto
 #-reduce_control_sets Auto
 #-fsm_extract YES -fsm_encoding Auto
 #-safe_implementation No
 #-fsm_style LUT
 #-ram_style Auto
 #-rom_extract Yes
 #-shreg_extract YES
 #-rom_style Auto
 #-auto_bram_packing NO
 #-async_to_sync NO
 #-use_dsp48 Auto
 #-iobuf YES
 #-register_duplication YES
 #-optimize_primitives NO
 #-use_clock_enable Auto
 #-use_sync_set Auto
 #-use_sync_reset Auto
 #-iob Auto
 #-equivalent_register_removal YES
 #-slice_utilization_ratio_maxmargin 5

 # Create the run-XST script
 set(XST_BUILD_LOG "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}_xst.log")
 set(XST_RUN_SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/runXST_${OUTFNAME}.sh")
 file(WRITE  ${XST_RUN_SCRIPT} "#!/bin/bash\n")
 file(APPEND ${XST_RUN_SCRIPT} "${XST} -intstyle xflow ${XILINX_FILTER_FLAG} -ifn ${XST_SCRIPT_FILE} -ofn ${XST_SYR_FILE} > ${XST_BUILD_LOG}\n")
 file(APPEND ${XST_RUN_SCRIPT} "if [ \"$?\" != \"0\" ]; then\n")
 file(APPEND ${XST_RUN_SCRIPT} "    cat ${XST_BUILD_LOG} | grep \"ERROR\"\n")
 file(APPEND ${XST_RUN_SCRIPT} "    exit 1;\n")
 file(APPEND ${XST_RUN_SCRIPT} "fi\n")
 file(APPEND ${XST_RUN_SCRIPT} "cat ${XST_SYR_FILE} | grep \"WARNING\"\n")
 file(APPEND ${XST_RUN_SCRIPT} "exit 0;\n")
 execute_process(COMMAND chmod +x ${XST_RUN_SCRIPT})
  
 # Synthesize
 set(NGC_FILE "${OUTFNAME}.ngc")
 add_custom_command(
  OUTPUT ${NGC_FILE}
  COMMAND ${XST_RUN_SCRIPT}
  DEPENDS ${SOURCES} ${TOP_LEVEL} ${UCF}
  COMMENT "Synthesizing NGC object ${NGC_FILE}"
 )
 
 # Create the run-NGDBUILD script
 set(NGDBUILD_LOG "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}_ngdbuild.log")
 set(NGDBUILD_RUN_SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/runNGDBUILD_${OUTFNAME}.sh")
 set(NGDBUILD_BLD_FILE "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}.bld")
 file(WRITE  ${NGDBUILD_RUN_SCRIPT} "#!/bin/bash\n")
 file(APPEND ${NGDBUILD_RUN_SCRIPT} "${NGDBUILD} -intstyle ise ${XILINX_FILTER_FLAG} -dd _ngo -nt timestamp -uc ${UCF} -p ${DEVICE} ${NGC_FILE} ${NGD_FILE} > ${NGDBUILD_LOG}\n")
 file(APPEND ${NGDBUILD_RUN_SCRIPT} "if [ \"$?\" != \"0\" ]; then\n")
 file(APPEND ${NGDBUILD_RUN_SCRIPT} "    cat ${NGDBUILD_LOG} | grep \"ERROR\"\n")
 file(APPEND ${NGDBUILD_RUN_SCRIPT} "    exit 1;\n")
 file(APPEND ${NGDBUILD_RUN_SCRIPT} "fi\n")
 file(APPEND ${NGDBUILD_RUN_SCRIPT} "cat ${NGDBUILD_BLD_FILE} | grep \"WARNING\"\n")
 file(APPEND ${NGDBUILD_RUN_SCRIPT} "exit 0;\n")
 execute_process(COMMAND chmod +x ${NGDBUILD_RUN_SCRIPT})
 
 # Translate
 set(NGD_FILE "${OUTFNAME}.ngd")
 set(PCF_FILE "${OUTFNAME}.pcf")
 add_custom_command(
  OUTPUT ${NGD_FILE}
  COMMAND ${NGDBUILD_RUN_SCRIPT}
  DEPENDS ${UCF} ${NGC_FILE}
  COMMENT "Translating NGD object ${NGD_FILE}"
 )
 
 # Create the run-MAP script
 set(MAP_NCD_FILE "${OUTFNAME}_map.ncd")
 set(MAP_LOG "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}_map.log")
 set(MAP_MRP_FILE "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}_map.mrp")
 set(MAP_RUN_SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/runMAP_${OUTFNAME}.sh")
 file(WRITE  ${MAP_RUN_SCRIPT} "#!/bin/bash\n")
 file(APPEND ${MAP_RUN_SCRIPT} "${MAP} -intstyle ise -p ${DEVICE} -w ${XILINX_FILTER_FLAG} -logic_opt off -ol high -t 1 -xt 0 -register_duplication off -r 4 -global_opt off -mt 2 -ir off -pr off -lc off -power off -o ${MAP_NCD_FILE} ${NGD_FILE} ${PCF_FILE} > ${MAP_LOG}\n")
 file(APPEND ${MAP_RUN_SCRIPT} "if [ \"$?\" != \"0\" ]; then\n")
 file(APPEND ${MAP_RUN_SCRIPT} "    cat ${MAP_LOG} | grep \"ERROR\"\n")
 file(APPEND ${MAP_RUN_SCRIPT} "    exit 1;\n")
 file(APPEND ${MAP_RUN_SCRIPT} "fi\n")
 file(APPEND ${MAP_RUN_SCRIPT} "cat ${MAP_MRP_FILE} | grep \"WARNING\"\n")
 file(APPEND ${MAP_RUN_SCRIPT} "exit 0;\n")
 execute_process(COMMAND chmod +x ${MAP_RUN_SCRIPT})
 
 # Map
 add_custom_command(
  OUTPUT ${MAP_NCD_FILE}
  COMMAND ${MAP_RUN_SCRIPT}
  DEPENDS ${UCF} ${NGD_FILE}
  COMMENT "Mapping native circuit description ${MAP_NCD_FILE}"
 )
 
 # Create the run-PAR script
 set(NCD_FILE "${OUTFNAME}.ncd")
 set(PAR_LOG "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}_par.log")
 set(PAR_RUN_SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/runPAR_${OUTFNAME}.sh")
 set(PAR_PAR_FILE "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}.par")
 file(WRITE  ${PAR_RUN_SCRIPT} "#!/bin/bash\n")
 file(APPEND ${PAR_RUN_SCRIPT} "${PAR} -w -intstyle ise ${XILINX_FILTER_FLAG}  -ol high -mt 4 ${MAP_NCD_FILE} ${NCD_FILE} ${PCF_FILE} > ${PAR_LOG}\n")
 file(APPEND ${PAR_RUN_SCRIPT} "if [ \"$?\" != \"0\" ]; then\n")
 file(APPEND ${PAR_RUN_SCRIPT} "    cat ${PAR_LOG} | grep \"ERROR\"\n")
 file(APPEND ${PAR_RUN_SCRIPT} "    exit 1;\n")
 file(APPEND ${PAR_RUN_SCRIPT} "fi\n")
 file(APPEND ${PAR_RUN_SCRIPT} "cat ${PAR_PAR_FILE} | grep \"WARNING\"\n")
 file(APPEND ${PAR_RUN_SCRIPT} "exit 0;\n")
 execute_process(COMMAND chmod +x ${PAR_RUN_SCRIPT})
 
 # PAR
 add_custom_command(
  OUTPUT ${NCD_FILE}
  COMMAND ${PAR_RUN_SCRIPT}
  DEPENDS ${UCF} ${MAP_NCD_FILE}
  COMMENT "Place and route native circuit description ${NCD_FILE}"
 )
 
 # Create the run-trce script
 set(TWX_FILE "${OUTFNAME}.twx")
 set(TWR_FILE "${OUTFNAME}.twr")
 set(TRCE_LOG "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}_trce.log")
 set(TRCE_RUN_SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/runTRCE_${OUTFNAME}.sh")
 file(WRITE  ${TRCE_RUN_SCRIPT} "#!/bin/bash\n")
 file(APPEND ${TRCE_RUN_SCRIPT} "${TRCE} -intstyle ise -v 3 -s 2 -n 3 ${XILINX_FILTER_FLAG}  -fastpaths -xml ${TWX_FILE} ${NCD_FILE} -o ${TWR_FILE} ${PCF_FILE} -ucf ${UCF} > ${TRCE_LOG}\n")
 file(APPEND ${TRCE_RUN_SCRIPT} "if [ \"$?\" != \"0\" ]; then\n")
 file(APPEND ${TRCE_RUN_SCRIPT} "    cat ${TRCE_LOG} | grep \"ERROR\"\n")
 file(APPEND ${TRCE_RUN_SCRIPT} "    exit 1;\n")
 file(APPEND ${TRCE_RUN_SCRIPT} "fi\n")
 file(APPEND ${TRCE_RUN_SCRIPT} "cat ${TWR_FILE} | grep \"0 timing errors detected\" > /dev/null\n")
 file(APPEND ${TRCE_RUN_SCRIPT} "if [ \"$?\" != \"0\" ]; then\n")
 file(APPEND ${TRCE_RUN_SCRIPT} "    cat ${TWR_FILE} | grep \"paths analyzed\"\n")
 file(APPEND ${TRCE_RUN_SCRIPT} "    cat ${TWR_FILE} | grep \"timing errors detected\"\n")
 file(APPEND ${TRCE_RUN_SCRIPT} "    cat ${TWR_FILE} | grep \"Minimum period is\"\n")
 file(APPEND ${TRCE_RUN_SCRIPT} "    cat ${TWR_FILE} | grep \"Score\"\n")
 file(APPEND ${TRCE_RUN_SCRIPT} "    exit 1;\n")
 file(APPEND ${TRCE_RUN_SCRIPT} "fi\n")
 execute_process(COMMAND chmod +x ${TRCE_RUN_SCRIPT})
 
 # TRCE
 add_custom_command(
  OUTPUT ${TWR_FILE}
  COMMAND ${TRCE_RUN_SCRIPT}
  DEPENDS ${UCF} ${NCD_FILE}
  COMMENT "Generate static timing analysis ${TWR_FILE}"
 )

 # Create the bitgen input script
 set(BITGEN_SCRIPT_FILE "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}.ut")
 set(BIT_FILE "${OUTFNAME}.bit")
 file(WRITE  ${BITGEN_SCRIPT_FILE} "-w\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g DebugBitstream:No\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g Binary:No\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g CRC:Enable\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g Reset_on_err:No\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g ConfigRate:2\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g ProgPin:PullUp\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g TckPin:PullUp\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g TdiPin:PullUp\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g TdoPin:PullUp\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g TmsPin:PullUp\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g UnusedPin:PullDown\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g UserID:0xFFFFFFFF\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g ExtMasterCclk_en:No\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g SPI_buswidth:1\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g TIMER_CFG:0xFFFF\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g multipin_wakeup:No\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g StartUpClk:CClk\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g DONE_cycle:4\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g GTS_cycle:5\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g GWE_cycle:6\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g LCK_cycle:NoWait\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g Security:None\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g DonePipe:Yes\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g DriveDone:Yes\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g en_sw_gsr:No\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g drive_awake:No\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g sw_clk:Startupclk\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g sw_gwe_cycle:5\n")
 file(APPEND ${BITGEN_SCRIPT_FILE} "-g sw_gts_cycle:4\n")
 
 # Create the run-bitgen script
 set(BITGEN_LOG "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}_bitgen.log")
 set(BITGEN_RUN_SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/runBITGEN_${OUTFNAME}.sh")
 set(BITGEN_BGN_FILE "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}.bgn")
 file(WRITE  ${BITGEN_RUN_SCRIPT} "#!/bin/bash\n")
 file(APPEND ${BITGEN_RUN_SCRIPT} "${BITGEN} -intstyle ise ${XILINX_FILTER_FLAG} -f ${BITGEN_SCRIPT_FILE} ${NCD_FILE} > ${BITGEN_LOG}\n")
 file(APPEND ${BITGEN_RUN_SCRIPT} "if [ \"$?\" != \"0\" ]; then\n")
 file(APPEND ${BITGEN_RUN_SCRIPT} "    cat ${BITGEN_LOG} | grep \"ERROR\"\n")
 file(APPEND ${BITGEN_RUN_SCRIPT} "    exit 1;\n")
 file(APPEND ${BITGEN_RUN_SCRIPT} "fi\n")
 file(APPEND ${BITGEN_RUN_SCRIPT} "cat ${BITGEN_BGN_FILE} | grep \"WARNING\"\n")
 file(APPEND ${BITGEN_RUN_SCRIPT} "exit 0;\n")
 execute_process(COMMAND chmod +x ${BITGEN_RUN_SCRIPT})
 
 # BITGEN
 # Must depend on trce in order for timing failure to prevent bitgen from running
 add_custom_target(
  ${OUTFNAME} ALL
  COMMAND ${BITGEN_RUN_SCRIPT}
  DEPENDS ${NCD_FILE} ${TWR_FILE}
  COMMENT "Generate FPGA bitstream ${BIT_FILE}"
  SOURCES ${NCD_FILE} ${TWR_FILE}
 )
 
 # Add additional make-clean files
 # Do not delete run scripts or toolchain input files, only outputs
 set_property(
  DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
  APPEND PROPERTY ADDITIONAL_MAKE_CLEAN_FILES
  ${XST_SYR_FILE}
  ${XST_BUILD_LOG}
  ${NGDBUILD_LOG}
  ${NGDBUILD_BLD_FILE}
  ${PCF_FILE}
  ${MAP_LOG}
  ${MAP_MRP_FILE}
  ${PAR_LOG}
  ${PAR_PAR_FILE}
  ${TWX_FILE}
  ${TRCE_LOG}
  ${BITGEN_LOG}
  ${BIT_FILE}
  ${BITGEN_BGN_FILE}
  "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}.lso"
  "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}.map"
  "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}_map.map"
  "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}_map.ngm"
  "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}_map.xrpt"
  "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}_ngdbuild.xrpt"
  "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}.ngm"
  "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}.pad"
  "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}_pad.csv"
  "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}_pad.txt"
  "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}_par.xrpt"
  "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}.ptwx"
  "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}_summary.xml"
  "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}.unroutes"
  "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}_usage.xml"
  "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}.xpi"
  "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}_xst.xrpt"
  "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}_bitgen.xwbt"
  "${CMAKE_CURRENT_BINARY_DIR}/${OUTFNAME}.drc"
  "${CMAKE_CURRENT_BINARY_DIR}/usage_statistics_webtalk.html"
  "${CMAKE_CURRENT_BINARY_DIR}/webtalk.log"
  "${CMAKE_CURRENT_BINARY_DIR}/par_usage_statistics.html"
  )

endfunction()

# TODO: planAhead
#planAhead -ise yes -m64 -log planAhead.log -journal planAhead.jou -source pa.fromNcd.tcl

#pa.fromHdl.tcl (pre-synthesis)
#create_project -name lx9-lvds-ioexpander -dir "/home/azonenberg/native/programming/verilogpractice/lx9-lvds-ioexpander/planAhead_run_1" -part xc6slx9tqg144-2
#set_param project.pinAheadLayout yes
#set srcset [get_property srcset [current_run -impl]]
#set_property target_constrs_file "TopLevel.ucf" [current_fileset -constrset]
#set hdlfile [add_files [list {TopLevel.v}]]
#set_property file_type Verilog $hdlfile
#set_property library work $hdlfile
#set_property top TopLevel $srcset
#add_files [list {TopLevel.ucf}] -fileset [get_property constrset [current_run]]
#open_rtl_design -part xc6slx9tqg144-2

#pa.fromNcd.tcl contents (post-PAR)
#create_project -name lx9-lvds-ioexpander -dir "/home/azonenberg/native/programming/verilogpractice/lx9-lvds-ioexpander/planAhead_run_1" -part xc6slx9tqg144-2
#set srcset [get_property srcset [current_run -impl]]
#set_property design_mode GateLvl $srcset
#set_property edif_top_file "/home/azonenberg/native/programming/verilogpractice/lx9-lvds-ioexpander/TopLevel.ngc" [ get_property srcset [ current_run ] ]
#add_files -norecurse { {/home/azonenberg/native/programming/verilogpractice/lx9-lvds-ioexpander} }
#set_property target_constrs_file "TopLevel.ucf" [current_fileset -constrset]
#add_files [list {TopLevel.ucf}] -fileset [get_property constrset [current_run]]
#link_design
#read_xdl -file "/home/azonenberg/native/programming/verilogpractice/lx9-lvds-ioexpander/TopLevel.ncd"
#if {[catch {read_twx -name results_1 -file "/home/azonenberg/native/programming/verilogpractice/lx9-lvds-ioexpander/TopLevel.twx"} eInfo]} {
#   puts "WARNING: there was a problem importing \"/home/azonenberg/native/programming/verilogpractice/lx9-lvds-ioexpander/TopLevel.twx\": $eInfo"
#}

Sunday, December 2, 2012

CMake, CTest, and CDash for Xilinx FPGAs

Despite most of my posts lately having been on hardware topics, my PhD work (as well as my undergraduate degree) is in computer science. As a result, one of my many interests is applying software development methodologies to hardware, soft hardware, and firmware.

This goes well beyond the obvious stuff like using version control for layout files or firmware source code. I'm talking about stuff like continuous integration and test-driven development. While I do commit frequently and have some unit tests (not as many as I'd like) there is currently no formal methodology for nightly builds, running all of the tests each commit (right now they need to be run by hand in the simulator), or automatic regression testing.

I've also been getting increasingly fed up with Xilinx's IDE lately (and IDEs in general, but that's another story...) - the editor doesn't support regex search and replace, all of the toolbars and wizards make it way too complex to change one compiler flag, and generally it seems to make me less productive. Almost all of my "pure" software projects use CMake with makefile outputs; I develop in a standalone editor and then just "make" from a shell to compile the code.

This post documents my work to date on a CMake-based workflow for Xilinx devices. My hope is that I can eventually have everything completely integrated so that my firmware, FPGA bitstreams, and RTL simulation test cases can all be built with a single "make" command.

The first part of my script is still very hackish - there are way too many hard-coded paths and it assumes the 14.3 toolchain version on 64-bit Linux, but it works for now and, more importantly, provides a wrapper that all of my other CMake code can use without changing even if I improve the autodetection.


# Find /opt/Xilinx or similar
find_file(XILINX_PARENT NAMES Xilinx PATHS /opt)
if(XILINX_PARENT STREQUAL "XILINX_PARENT-NOTFOUND")
 message(FATAL_ERROR "No Xilinx toolchain installation found")
endif()

# Find /opt/Xilinx/VERSION
# TODO: Figure out a better way of doing this
find_file(XILINX NAMES 14.3 PATHS ${XILINX_PARENT})
if(XILINX STREQUAL "XILINX-NOTFOUND")
 message(FATAL_ERROR "No ISE 14.3 installation found")
endif()
message(STATUS "Found Xilinx toolchain... ${XILINX}")

# Set current OS architecture (TODO: autodetect)
set(XILINX_ARCH lin64)

# Find fuse
find_file(FUSE NAMES fuse PATHS "${XILINX}/ISE_DS/ISE/bin/${XILINX_ARCH}/")
if(FUSE STREQUAL "FUSE-NOTFOUND")
 message(FATAL_ERROR "No Xilinx fuse installation found")
endif()
message(STATUS "Found Xilinx fuse... ${FUSE}")

The next step was a little helper for argument parsing. In most languages supported by CMake there is no concept of a "top level" source file - the compiler finds your main() function but the build system doesn't need to know which file it's in.

macro(xilinx_parse_args _top_level _sources)
 set(${_top_level} FALSE)
 set(${_sources})
 set(_found_sources FALSE)
 set(_found_top_level FALSE)
 foreach(arg ${ARGN})
  if(${arg} STREQUAL "TOP_LEVEL")
   set(_found_top_level TRUE)
  elseif(${arg} STREQUAL "SOURCES")
   set(_found_sources TRUE)
  elseif(${_found_sources})
   list(APPEND ${_sources} ${arg})
  elseif(${_found_top_level})
   if(${_top_level})
    message(FATAL_ERROR "Multiple top-level files specified in xilinx_parse_args")
   else()
    set(${_top_level} ${arg})    
   endif()
  else()
   message(FATAL_ERROR "Unrecognized command ${arg} in xilinx_parse_args")
  endif()
 endforeach()
endmacro()

Once this was working it was time to actually create a simulation executable. This is a bit more involved than one might think for a couple of reasons:
  • The Xilinx tools ship their own custom C/C++ runtime libraries which are generally not the same version as that used by the host system. If you source /opt/Xilinx/[VERSION]/ISE_DS/settings[32|64].sh then all of the tools work (and are added to your $PATH) but any application depending on the host's glibc version won't start!
  • As a result, CMake and CTest require the host's glibc (so you can't run the sim executable or it'll segfault) and the sim executable requires the Xilinx glibc. This means that CTest cannot run the sim executable directly.
  • ISim does not seem to provide any way of setting the exit code for a simulation. CTest expects a test case to return 0 on success and nonzero on failure.
I started out by creating a tcl script (pretty much an exact copy of the one generated by the GUI toolchain except for the exit call) to run the simulation. Right now the 1000ns run time is hard coded so your simulation must finish sooner than that or not all the test cases will run. I'm going to make this parameterizable in the future.

onerror {resume}
wave add /
run 1000 ns;
exit;

This script is then launched by an automatically generated bash script which runs the simulation and then looks at the log file. If your simulation ever issues a $DISPLAY with the text "FAIL" in it, the test case is considered a failure; otherwise it's marked a success. The intention is to have a bunch of test cases in the testbench printing out something like "SPI flash read test: [PASS|FAIL]"

#!/bin/bash
cd /home/azonenberg/native/programming/verilogpractice/UnitTest02/build/tests/testNandGate
source /opt/Xilinx/14.3/ISE_DS/settings64.sh
./testNandGate -tclbatch /home/azonenberg/native/programming/verilogpractice/UnitTest02/build/tests/testNandGate/testNandGate.tcl -intstyle silent -vcdfile testNandGate.vcd -vcdunit ps || exit 1
cat isim.log | grep -q FAIL
if [ "$?" != "1" ]; then
    exit 1;
fi

Gluing all of the necessary parts and code generation together, we get this:
function(add_isim_executable OUTPUT_FILE )
  
 # Parse args
 xilinx_parse_args(TOP_LEVEL SOURCES ${ARGN})
 
 # Get base name without extension of the top-level module
 get_filename_component(TOPLEVEL_BASENAME ${TOP_LEVEL} NAME_WE )
 
 # Write the .prj file
 set(PRJ_FILE "${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_FILE}.prj")
 file(WRITE ${PRJ_FILE} "verilog work \"${TOP_LEVEL}\"\n")
 foreach(f ${SOURCES})
  file(APPEND ${PRJ_FILE} "verilog work \"${f}\"\n")
 endforeach()
 file(APPEND ${PRJ_FILE} "verilog work \"${XILINX}/ISE_DS/ISE/verilog/src/glbl.v\"\n")
 
 # Main compile rule
 # TODO: tweak this
 add_custom_target(
  ${OUTPUT_FILE} ALL
  COMMAND ${FUSE} ${FUSE_FLAGS} -o ${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_FILE} -prj ${PRJ_FILE}
    work.${TOPLEVEL_BASENAME} work.glbl > ${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_FILE}_build.log
    2> ${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_FILE}_err.log
  DEPENDS ${SOURCES} ${TOP_LEVEL}
  COMMENT "Building ISim executable ${OUTPUT_FILE}..."
 )
 
 # Write the tcl script
 set(TCL_FILE "${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_FILE}.tcl")
 file(WRITE ${TCL_FILE} "onerror {resume}\n")
 file(APPEND ${TCL_FILE} "wave add /\n")
 file(APPEND ${TCL_FILE} "run 1000 ns;\n")
 file(APPEND ${TCL_FILE} "exit;\n")
 
 # Write the run-test wrapper script
 set(TEST_WRAPPER "${CMAKE_CURRENT_BINARY_DIR}/run${OUTPUT_FILE}.sh")
 file(WRITE ${TEST_WRAPPER} "#!/bin/bash\n")
 file(APPEND ${TEST_WRAPPER} "cd ${CMAKE_CURRENT_BINARY_DIR}\n")
 file(APPEND ${TEST_WRAPPER} "source ${XILINX}/ISE_DS/settings64.sh\n")
 file(APPEND ${TEST_WRAPPER} "./${OUTPUT_FILE} -tclbatch ${TCL_FILE} -intstyle silent -vcdfile ${OUTPUT_FILE}.vcd -vcdunit ps || exit 1\n")
 file(APPEND ${TEST_WRAPPER} "cat isim.log | grep -q FAIL\n")
 file(APPEND ${TEST_WRAPPER} "if [ \"$?\" != \"1\" ]; then\n")
 file(APPEND ${TEST_WRAPPER} "    exit 1;\n")
 file(APPEND ${TEST_WRAPPER} "fi\n")
 add_custom_command(TARGET ${OUTPUT_FILE} POST_BUILD COMMAND chmod +x ${TEST_WRAPPER})
 
endfunction()

There are several issues with the system right now; the most notable is that the fuse command is run every build even if the source files haven't changed. I'm going to fix this in a future release; this is just a WIP.

The final piece of the puzzle is some glue to create the executable and a CTest test case that calls the bash script:

function(add_isim_test TEST_NAME)

 # Parse args
 xilinx_parse_args(TOP_LEVEL SOURCES ${ARGN})

 # Add the sim executable
 add_isim_executable(test${TEST_NAME}
  TOP_LEVEL
   ${TOP_LEVEL}
  SOURCES 
   ${SOURCES}
  )

 add_test(${TEST_NAME}
  "${CMAKE_CURRENT_BINARY_DIR}/runtest${TEST_NAME}.sh")
 set_property(TEST ${TEST_NAME} APPEND PROPERTY DEPENDS test${TEST_NAME})


endfunction()

This is the only external interface to the whole module for now; everything else is just internal helper routines. The intended use is as follows:
cmake_minimum_required(VERSION 2.8)
include(FindXilinx.cmake)
enable_testing()
include (CTest)
add_isim_test(NandGate
 TOP_LEVEL
  ${CMAKE_CURRENT_SOURCE_DIR}/testNandGate.v
 SOURCES 
  ${CMAKE_SOURCE_DIR}/hdl/NandGate.v
 )

The full code for FindXilinx.cmake as it stands is here for convenience:
########################################################################################################################
# @file FindXilinx.cmake
# @author Andrew D. Zonenberg
# @brief Xilinx ISE toolchain CMake module
########################################################################################################################

########################################################################################################################
# Autodetect Xilinx paths (very hacky for now)

# Find /opt/Xilinx or similar
find_file(XILINX_PARENT NAMES Xilinx PATHS /opt)
if(XILINX_PARENT STREQUAL "XILINX_PARENT-NOTFOUND")
 message(FATAL_ERROR "No Xilinx toolchain installation found")
endif()

# Find /opt/Xilinx/VERSION
# TODO: Figure out a better way of doing this
find_file(XILINX NAMES 14.3 PATHS ${XILINX_PARENT})
if(XILINX STREQUAL "XILINX-NOTFOUND")
 message(FATAL_ERROR "No ISE 14.3 installation found")
endif()
message(STATUS "Found Xilinx toolchain... ${XILINX}")

# Set current OS architecture (TODO: autodetect)
set(XILINX_ARCH lin64)

# Find fuse
find_file(FUSE NAMES fuse PATHS "${XILINX}/ISE_DS/ISE/bin/${XILINX_ARCH}/")
if(FUSE STREQUAL "FUSE-NOTFOUND")
 message(FATAL_ERROR "No Xilinx fuse installation found")
endif()
message(STATUS "Found Xilinx fuse... ${FUSE}")

########################################################################################################################
# Argument parsing helper

macro(xilinx_parse_args _top_level _sources)
 set(${_top_level} FALSE)
 set(${_sources})
 set(_found_sources FALSE)
 set(_found_top_level FALSE)
 foreach(arg ${ARGN})
  if(${arg} STREQUAL "TOP_LEVEL")
   set(_found_top_level TRUE)
  elseif(${arg} STREQUAL "SOURCES")
   set(_found_sources TRUE)
  elseif(${_found_sources})
   list(APPEND ${_sources} ${arg})
  elseif(${_found_top_level})
   if(${_top_level})
    message(FATAL_ERROR "Multiple top-level files specified in xilinx_parse_args")
   else()
    set(${_top_level} ${arg})    
   endif()
  else()
   message(FATAL_ERROR "Unrecognized command ${arg} in xilinx_parse_args")
  endif()
 endforeach()
endmacro()

########################################################################################################################
# ISim executable generation

function(add_isim_executable OUTPUT_FILE )
  
 # Parse args
 xilinx_parse_args(TOP_LEVEL SOURCES ${ARGN})
 
 # Get base name without extension of the top-level module
 get_filename_component(TOPLEVEL_BASENAME ${TOP_LEVEL} NAME_WE )
 
 # Write the .prj file
 set(PRJ_FILE "${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_FILE}.prj")
 file(WRITE ${PRJ_FILE} "verilog work \"${TOP_LEVEL}\"\n")
 foreach(f ${SOURCES})
  file(APPEND ${PRJ_FILE} "verilog work \"${f}\"\n")
 endforeach()
 file(APPEND ${PRJ_FILE} "verilog work \"${XILINX}/ISE_DS/ISE/verilog/src/glbl.v\"\n")
 
 # Main compile rule
 # TODO: tweak this
 add_custom_target(
  ${OUTPUT_FILE} ALL
  COMMAND ${FUSE} ${FUSE_FLAGS} -o ${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_FILE} -prj ${PRJ_FILE}
    work.${TOPLEVEL_BASENAME} work.glbl > ${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_FILE}_build.log
    2> ${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_FILE}_err.log
  DEPENDS ${SOURCES} ${TOP_LEVEL}
  COMMENT "Building ISim executable ${OUTPUT_FILE}..."
 )
 
 # Write the tcl script
 set(TCL_FILE "${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_FILE}.tcl")
 file(WRITE ${TCL_FILE} "onerror {resume}\n")
 file(APPEND ${TCL_FILE} "wave add /\n")
 file(APPEND ${TCL_FILE} "run 1000 ns;\n")
 file(APPEND ${TCL_FILE} "exit;\n")
 
 # Write the run-test wrapper script
 set(TEST_WRAPPER "${CMAKE_CURRENT_BINARY_DIR}/run${OUTPUT_FILE}.sh")
 file(WRITE ${TEST_WRAPPER} "#!/bin/bash\n")
 file(APPEND ${TEST_WRAPPER} "cd ${CMAKE_CURRENT_BINARY_DIR}\n")
 file(APPEND ${TEST_WRAPPER} "source ${XILINX}/ISE_DS/settings64.sh\n")
 file(APPEND ${TEST_WRAPPER} "./${OUTPUT_FILE} -tclbatch ${TCL_FILE} -intstyle silent -vcdfile ${OUTPUT_FILE}.vcd -vcdunit ps || exit 1\n")
 file(APPEND ${TEST_WRAPPER} "cat isim.log | grep -q FAIL\n")
 file(APPEND ${TEST_WRAPPER} "if [ \"$?\" != \"1\" ]; then\n")
 file(APPEND ${TEST_WRAPPER} "    exit 1;\n")
 file(APPEND ${TEST_WRAPPER} "fi\n")
 add_custom_command(TARGET ${OUTPUT_FILE} POST_BUILD COMMAND chmod +x ${TEST_WRAPPER})
 
endfunction()

########################################################################################################################
# Test generation
#
# Usage:
# add_isim_test(NandGate
# TOP_LEVEL
#  ${CMAKE_CURRENT_SOURCE_DIR}/testNandGate.v
# SOURCES 
#  ${CMAKE_SOURCE_DIR}/hdl/NandGate.v
# )

function(add_isim_test TEST_NAME)

 # Parse args
 xilinx_parse_args(TOP_LEVEL SOURCES ${ARGN})

 # Add the sim executable
 add_isim_executable(test${TEST_NAME}
  TOP_LEVEL
   ${TOP_LEVEL}
  SOURCES 
   ${SOURCES}
  )

 add_test(${TEST_NAME}
  "${CMAKE_CURRENT_BINARY_DIR}/runtest${TEST_NAME}.sh")
 set_property(TEST ${TEST_NAME} APPEND PROPERTY DEPENDS test${TEST_NAME})


endfunction()
Once I get it to a more stable state I'll probably set up a Google Code page but for now this is good enough. The code can be used under the same 3-clause BSD license as almost all of my other open source code.

Thursday, October 18, 2012

Cross-sectioning setup


My roommate Rob has a Unimat multipurpose machine tool which had been sitting around for a long time not being used for anything since we already have a full sized mill and lathe in our apartment shop.

We came up with the bright idea a few months ago of using it as a cross-sectioning saw by setting it up in the lathe configuration and putting a Dremel abrasive cut-off disk in the chuck. We then put the milling table on the carriage and clamped the workpiece to that. The end result is a tiny little abrasive-bladed cutoff saw, as used in most of my previous PCB/BGA cross section photos.

Unimat tool set up for cross-sectioning
The PVC plumbing visible in the background is part of our workbench dust-collection setup. A shop vac (off the left side of the frame on the floor) sucks through a 2.5" pipe with a bunch of T connectors on it. Each T is then necked down to 1.25" and has a ball valve before going out to an overhanging arm which is press-fit (rather than solvent welding as used for the permanent parts) together and connected with screw unions in critical spots.

The end result is that we can position each arm directly over the location of the cut and turn on suction for that intake only.

Sunday, October 14, 2012

Dummy BGAs and failure analysis

I'm back...  it's been a while since my last post so I figured I'd write something. I spent most of the summer working on my PhD research and don't have anything ready to publish on that, but I'm now starting design work on a new development board that will tentatively use an Artix-7 FPGA in FGG484 package.

The chip is going to be quite expensive and will be on a six-layer board (also not cheap) so I decided to do some research to characterize my BGA process a bit better as well as improving yields once failure sources can be identified.

I began by designing two mating PCBs in FT[G]256 form factor (pictured below) and buying a jar of 250,000 SAC305 solder balls. The contact-chain pattern was structured such that every ball was electrically isolated from the ones immediately up, down, left, and right, and connected to those diagonally opposite via a leapfrog-zigzag pattern. The end result is two chains of 128 balls in series, so that any open circuit can be detected, which should be electrically isolated. All possible horizontal or vertical shorts would be detectable as a short between the two chains.

Dummy FTG256 component

Dummy FTG256 carrier board (probe pads at top and bottom cropped)

The next step once the boards came back from fab was to take one of the dummy components and ball it.

I began by smearing the board with sticky flux using a microfiber swab. I need to come up with a good way of depositing thin films of sticky flux (a stencil of some sort maybe? thin and spin coating) uniformly over a board... the amount pictured turned out to be too much.

Fluxing the board
The next step was to begin placing solder balls. Lacking a stencil I just used tweezers to place them one at a time. It took a while but as long as I won't be doing this very often I can't justify the cost.

Beginning to place solder balls
Close-up of placed solder balls before reflow
Since this was just a test I decided to reflow the first half of the board to see how it turned out.

A minute or so into the reflow profile it was obvious something was wrong - the solder balls were moving all over the place.

Drifting solder balls
Close-up of drifting solder balls
It appeared that as the gel-based flux liquified, thickness variations caused it to flow and take solder balls with it. Surface tension resulted in balls trying to cling to one another.

I removed some of the excess flux, repositioned the misaligned balls, and reflowed, then repeated for the rest of the balls. A few of the balls moved again and bridged together so I removed them with solder braid, re-fluxed, and reflowed again with new balls.

Another defect visible post-reflow. For some reason this ball never quite made contact with the pad. It seemed to be fine after reflow.
The entire dummy component after reflow (whitish residue was left by flux after cleaning)
After reflow I took a quick look at the board and everything seemed fine, there was a ball on each pad and nothing was shorting.

I then treated the resulting board as an FTG256 component and reflowed it to the carrier board using my standard profile.

The resulting assembly passed the "no shorts" test and the "continuity of chain 1" test but the other chain showed an open circuit. After sanding the soldermask off the back of the dummy component (in retrospect I should have left the vias open for easy probing) a binary search quickly determined that pads F8 and F10, which should have been connected, were not. At this point it wasn't known which of the two connections was open.

I then cross-sectioned the board several rows back from the F row to get a general look at how the reflow had gone. I made the cut slightly off parallel so that I could get a slice through some of the balls as well as seeing the dog-bones and vias.

After making the cut I de-fluxed with a high-pressure stream of 50% v/v acetone/IPA from a syringe.

Close-up of two balls showing saw marks (very quickly polished). The apparent void on the left-hand ball is actually diamond abrasive paste on top of the ball, not a solder defect.

Cross section of the board. Note that the cut is slightly off parallel to the balls; each ball is cut slightly higher than the one to its left and the right-hand two are not cut at all.
Everything looked good - the balls had clearly flowed around the sides of the NSMD pads and were showing good adhesion, none of them were distorted or anywhere near shorting, and there were no visible cracks or other defects.

I then made another cut just before the F row in hopes of locating the actual defect. I didn't even need to use the microscope to see something was wrong - there was no ball in the F10 position whatsoever!

Missing solder ball!
At this point I was quite confused because I knew that I had put a ball on every pad. I decided to polish a tiny bit closer and get some more images.

The pad on the carrier board (bottom) can be observed to still have the gold plating on it. There is no evidence of tinning whatsoever.
After seeing that the pad on the board was completely un-tinned and gold plated, it seemed that the ball had not adhered to the pad at all.

I then went back and looked at the post-balling picture of the board. What had originally escaped my notice was that ball F10 (origin at lower right, up six, left ten) was a lot smaller than the others. It's not clear what happened but I'm guessing that while removing shorted balls with braid I accidentally sucked some of the solder off that pad.

I'm not certain that this is the correct explanation yet but it fits the data well and is simple. I'll be doing several more dummy BGAs over the coming weeks to see how things turn out.

Tuesday, July 24, 2012

I've joined the CMOSfold team :)

I've been added as a contributing author to CMOSfold, the "weekly centerfold" of nude semiconductors run by my friend and colleague John McMaster. It contains a large number of brief overviews and top-metal photos of various chips, without much analysis.

I'll try and do a post every week or two there on a random chip I have in my personal collection. They may later be followed by more in-depth analysis here.