Every mixed-signal system needs some form of a digital sequencing which controls the internal blocks. Typically for test systems it is highly desirable that this sequencer is also flexible. While there are a number of ways of achieveing this, I am sharing a tool and perhaps a solution for generating control signals for testchips based on an external FPGA-based ROM memory block. This approach may not be the most efficient when it comes to silicon real estate or power. However, what I like in this methodology is that it is extremely flexible, and yet is relatively simple to work with. Fiddling with dedicated state machines is perhaps the most efficient methodology but it is a bit more time consuming for inexperienced people in digital design as myself.
To start with, here's an overview of an example system:
An FPGA contains a RAM/ROM block, together with some control circuitry. Pretty simple huh? And it truly is. The UART module clocks-in the memory content to the write and read address generator, which loads the data into the memory. After the memory is initialized, all the address generator does is to loop through the RAM's depth (addresses).
One can generate the RAM block using the IPcore generator in Xilinx's Vivado. If you use other vendors I am sure they would be offering similar tools. What you need to take care of is the memory's width, which is driven by the number of sequener signals you need, as well as its depth — depends on the sequencer resolution you are targeting, as well as its length. For most cases, if you use a large FPGA you shouldn't run out of dedicated memory block space. Here's an example screenshot oof Xilinx's IPcore generator:
There's plenty of ways to generate memories with various tools. If one has to write the memory content bit-by-bit it'll take him, perhaps not years, but hours. Fiddling with raw "1/0" text files, boxes, csv, or whatever else is a painful task.
To try and automate things a little bit, I created a tiny assembler instruction interpreter, which compiles memory machine code, which I can then directly load into the memory via the UART. This is currently a work in progress, however, I have put a plenty of code comments, and I've tried to keep things clean. Hehe, well, as much as perl code can be made clean.
The instruction list is currently embedded into the code and remains under the parse_line() subroutine, where you can swap bit posisions, instruction syntax and add new functionality. There are two global definitions in the main() subroutine needed to be taken care of. These are the: \$ram_width and \$ram_depth. The latter define the exact size of the memory file and must match with the ones used during the generation of the IP core, as well as the address length of the memory addresser module. Here's how the memory content file looks like:
memory_initialization_radix=2; memory_initialization_vector= 00000100111010011001111010100000, 00000100111010011001111010100000, 00000100111010011001111010100000, 00000000111110011001100011100000, 00000000111110011001100011100000, 00000000111010011111110011100000, 00000000111010011110110011100000, 00000000111010011110110011100000, 00000000111010011110110011100000, 00000000111010011111110011100000, ...
Each instruction, when called, alters a specific bit (also may be multiple bits) in the memory. Right now the tool has a set of 30ish instructions. Here's a complete list:
;|------------------------------------------------------------------| ;| Instruction List and Function | ;|------------------------------------------------------------------| ;| ROW 0x00 1/0 - set row_rs | ;| ROW 0x01 1/0 - set row_rst | ;| ROW 0x02 1/0 - set row_tx | ;| ROW 0x03 1/0 - set col_bias_sh | ;| ROW 0x04 1/0 - set row_next | ;| SHX 0x00 1/0 - set shr | ;| SHX 0x01 1/0 - set shs | ;| ADX 0x00 1/0 - set adr | ;| ADX 0x01 1/0 - set ads | ;| COM 0x00 1/0 - set comp_bias_sh | ;| COM 0x01 1/0 - set comp_dyn_pon | ;| CNT 0x00 1/0 - set count_en | ;| CNT 0x01 1/0 - set count_rst | ;| CNT 0x02 1/0 - set count_inv_clk | ;| CNT 0x03 1/0 - set count_hold | ;| CNT 0x04 1/0 - set count_updn | ;| CNT 0x05 1/0 - set count_inc_one | ;| CNT 0x06 1/0 - set count_jc_shift_en | ;| CNT 0x07 1/0 - set count_lsb_en | ;| CNT 0x08 1/0 - set count_lsb_clk | ;| MEM 0x00 1/0 - set count_mem_wr | ;| REF 0x00 1/0 - set ref_vref_ramp_rst | ;| REF 0x01 1/0 - set ref_vref_sh | ;| REF 0x02 1/0 - set ref_vref_clamp | ;| REF 0x03 1/0 - set ref_vref_ramp_ota_dyn_pon | ;| SER 0x00 1/0 - set digif_seraial_rst | ;| LOAD PAR - load follow-up instructions to buf register | ;| SET PAR - set the loaded in buf register to output | ;| START - initialize output register to 0x0000 | ;| NOP - NOP operation (stall) one cycle | ;| NOP n - NOP operation (stall) n cycles | ;| FVAL 0x00 1/0 - frame valid | ;| LVAL 0x00 1/0 - line valid | ;|------------------------------------------------------------------|
I tried to kind-of preserve the classic assembler language constructs as much as I could, e.g. comments use ";", to load a signal you start with MOV, then issue the command e.g. ROW followed by its specific bit e.g. 0x01 and finally the value to be written. The NOP instructions stall the system (copy the previous memory line for N cycles), LOAD PAR pushes the next instructions into the PAR register, afterwards SET PAR sets the modified instructions after LOAD PAR, in parallel. Here's a sample code:
START ;|----------------------------| ;| Initialize startup signals | ;|----------------------------| LOAD PAR MOV REF 0x03 1 ; ota_dyn_pon always @ '1' MOV CNT 0x04 1 ; count_updn '1' MOV CNT 0x01 1 ; count_rst '1' MOV CNT 0x05 1 ; count_inc_one '1' MOV CNT 0x08 1 ; count_lsb_clk '1' MOV MEM 0x00 1 ; count_mem_wr '1' MOV FVAL 0x00 1 ; FVAL '1' MOV COM 0x01 1 ; comp_dyn_pon always @ '1' ;|-----------------| ;| Sequencer start | ;|-----------------| ; references and shr sampling MOV ADX 0x00 1 ; adr MOV SHX 0x00 1 ; shr MOV REF 0x01 1 ; ref_vref_sh MOV REF 0x00 1 ; ref_vref_ramp_rst MOV COM 0x00 1 ; comp_bias_sh SET PAR NOP 22 ; halt 220 ns - phase 1 in vref_ramp LOAD PAR MOV REF 0x01 0 ; vref_sh SET PAR NOP 8 ; halt 80 ns MOV REF 0x02 0 ; cla off NOP 6 ; reset counter LOAD PAR MOV CNT 0x04 0 MOV CNT 0x01 0 MOV CNT 0x05 0 SET PAR NOP 2 LOAD PAR MOV CNT 0x04 1 MOV CNT 0x01 1 MOV CNT 0x05 1 SET PAR NOP NOP 52 ; halt 520 ns - wait for ramp buffer to settle MOV SER 0x00 1 ; stop data serialization out NOP 34 ; prehalt - wait SHA MOV SHX 0x00 0 ; complete shr sampling NOP 2 ; start count + ramp current LOAD PAR MOV REF 0x00 0 MOV CNT 0x00 1 SET PAR NOP 102 ; halt 1024 ns (ramp slew time) MOV CNT 0x00 0 ; stop counter MOV REF 0x00 1 ; stop ramp current ...
The execution of the script is rather simple:
asmitp.pl program.asm -o instr.coe
Future to do:
1. Put instruction table setting in an external file
2. Add a vec and vcd file generation switch, to allow integration with spectre/spice
3. Implement JUMP instruction (loop)
4. Implement conditional statements
5. Create pdf documentation
If you find this approach appealing, please feel free to fork my repo or write to me so that I can add you as a contributor and develop these tools further. Cheers ;)