-
What if we generated the files for each gate ?
06/12/2019 at 00:35 • 0 commentsThere are about 100 files of the gate_* form. They are quite hard to manage, though the collection has grown organically and slowly... but if we want to modify them globally, it becomes painful and tedious. Errors could creep into the code and be hard to spot...
Most gates follow a simple pattern : 3 inputs, one output, one boolean function to apply. It seems very easy to script.
Then, there are some gates with a different naming scheme, such as the MUXx gates. OK, so let's have, for each gate, an array of strings with the names of the input signals.
Then, there are gates with only 2, 1 (INV) and even 0 inputs (GND...) so a different/smaller lookup function is required.
And then, there are the sequential gates... Complexity gets out of control because there are many variations that change the code. That part will be a lot harder to script.
Then there is this crucial question : what language to use ? VHDL is perfectly capable of performing the generation, as it can access files and process character strings. It's good because there would be a single language, and the generation can be performed only once in a while. However it's not the MOST appropriate language. I'd do it in JavaScript if it was easier to use on the command line... C is not a contender and I'm not keen on Python. It would be awkward in bash as well...
-
What about coverage ?
06/11/2019 at 22:06 • 0 commentsAs this library exceeds the initial limited scope, new perspectives emerge. It's good to have a library that replicates the historical Actel gates, it's awesome to inject faults, but now, what about we collect stats about the gates activities ?
Actel/MicroSemi/Microchip already does something similar during simulation to estimate power consumption but my idea is different : IF I'm going to alter a logic gate by 1 bit, then I need to know if this input combination really has a use. IF I can detect that an input combination is not used in practice, it could help simplify the gate for example. And it would also mean that it's useless to alter this bit.
So it's a "coverage" test, that gathers a histogram of the meaningful input combinations and helps direct test vectors toward rarely-seen cases (while the common cases are usually degenerate and are often covered "along" when the rare cases are tested)
Testing coverage requires a good testbench that stimulates all the inputs and intermediate signals.Then, the testbench runs with all the gates tooled with a histogram (that could saturate at 255 or 64Ki). The trick is to sample these input values when the clock is about to change because the delta cycles will add transients with meaningless values. It makes the implementation harder because we don't want to add an additional clock input to each gate... but VHDL or VHPI can do wonders.
At each sampling point, the inputs of each gate are collected into a number (0 to 7) that serves as an index into the histogram, where one bin is incremented.
After the run, the histogram is collected and sorted, lowest bins first : these are the ones that the test vectors must "hit" with one test vector. If possible the test vector also hits more "low bins" to increase efficiency and reduce the number of test vectors (and testing time).
How a test vector can be created from a given target configuration is not yet easy or examined...
...
The list of vectors can then be used to enhance the testbench (or make another, hopefully shorter). Null bins are not used and thus are not tested because they would create a "false fail" condition. However the null bins are useful to detect design defects.
-
Generalised fault injection - part deux
06/10/2019 at 19:17 • 0 commentsThe idea progresses while I gather the necessary ingredients for the new version of the library. At this moment I consider getting rid of all the C and VHPI code because VHDL already contains all the required features, I think.
Let's start with the fundamental feature, that evaluates the gate's function. It must be short, simple, efficient, fast. We work with SLV8 (std_logic_vector(7 downto 0)) as the boolean function table.
subtype FunctTableType is std_logic_vector( 7 downto 0);
The input signal A has the least significant effect on the index, so we have the linear relationship :
C B A Index 0 0 0 0 0 0 1 1 0 1 0 2 0 1 1 3 1 0 0 4 1 0 1 5 1 1 0 6 1 1 1 7 OR3 is coded as "11111110" and AND3 as "10000000".
From there, it's very easy to look it up :
function TableLookup( A : std_logic ; B : std_logic ; C : std_logic ; Table : FunctTableType) return std_logic is variable index : integer := 0; begin if A = '1' then index := 1; end if; if B = '1' then index := index + 2; end if; if C = '1' then index := index + 4; end if; return Table(index); end function;
There. No C code required.
Then the body of the gate's entity applies this function to the corresponding table.
entity AND3 is port( A, B, C : in std_logic; Y : out std_logic); end AND3; architecture rtl of AND3 is constant Table : FuncTableType := "10000000"; begin Y <= TableLookup(A, B, C, Table) after gate_delay; end rtl;
Nothing complex, here, either, though I have moved the constant for a separated declaration, instead of a mere inline mention. That's where the hinge of the system resides.
First, we must initialise the function table.
Initially I wanted it to be initialised in the gate file itself. It would make the system self-contained, reducing confusion and complexity. Then I realised that the initialisation (and table creation) would be computed, again and again, for every gate in the DUT. It's not an issue for the example INC8 unit but imagine a huge processor... So it's best to create all the tables once and for all, before everything. And since I don't want to do it by hand, some code is required (even though it will take more dev time, huh huh huh).
I want to reduce the duplication of code as much as possible and at that moment, it is VERY tempting to create only one entity/architecture, with a function table provided as a generic but this would break the compatibility with the historical ACTEL files. As a consequence, the collection of gate files will continue to exist...
However the constants can be precomputed in the package.
Library ieee; use ieee.std_logic_1164.all; package proasic3 is subtype FunctTableType is std_logic_vector( 7 downto 0); type A3PTileName is ( AND2, AND2A, AND3, AND3A, AND3B, AND3C, AO1, AO12, AO13, AO14, AO15, AO16, AO17, AO18, AO1A, AO1B, AO1C, AO1D, AO1E, AOI1, AOI1A, AOI1B, AOI1C, AOI1D, AOI5, AX1, AX1A, AX1B, AX1C, AX1D, AX1E, AXO1, AXO2, AXO3, AXO5, AXO6, AXO7, AXOI1, AXOI2, AXOI3, AXOI4, AXOI5, AXOI7, CLKINT, DFN1, DFN1C0, DFN1C1, DFN1E0, DFN1E0C0, DFN1E0C1, DFN1E0P0, DFN1E0P1, DFN1E1, DFN1E1C0, DFN1E1C1, DFN1E1P0, DFN1E1P1, DFN1P0, DFN1P1, INV, MAJ3, MX2, MX2A, MX2B, MX2C, NAND2, NAND3A, NAND3B, NAND3, NOR2A, NOR2, NOR2B, NOR3, NOR3A, NOR3B, NOR3C, OA1A, OA1B, OA1C, OAI1, OR2, OR2A, OR2B, OR3, OR3A, OR3B, OR3C, XA1, XA1A, XA1B, XA1C, XNOR2, XNOR3, XO1, XO1A, XOR2, XOR3, ZOR3, ZOR3I); function Create_Table(f : A3PTileName) return FunctTableType; function TableLookup( A, B, C : std_logic; Table : FunctTableType) return std_logic; constant A3P_AND3 : FunctTableType := Create_Table(AND3); ... component AND3 port(A, B, C : in std_logic; Y : out std_logic); end component; ... end proasic3; package body ygrec8_def is function TableLookup( A, B, C : std_logic ; Table : FunctTableType) return std_logic is variable index : integer := 0; begin if A = '1' then index := 1; end if; if B = '1' then index := index + 2; end if; if C = '1' then index := index + 4; end if; return Table(index); end function; end ygrec8_def;
I must admit that at this point, I have tested nothing so I'm wildly speculating, but you might see where I'm going.
All the constants are created by a single function that contains all the boolean code in a large case().
function Create_Table(f : A3PTileName) return FunctTableType is variable i : integer := 0; variable A, B, C, Y : std_logic; variable t : FunctTableType; begin C := '0'; loop_C: loop B := '0'; loop_B: loop A := '0'; loop_A: loop -- nested loop's body: Y := .... -- huge case() here t[i] := Y; i := i+1; exit loop_A when A = '1'; A := '1'; end loop loop_A; exit loop_B when B = '1'; B := '1'; end loop loop_B; exit loop_C when C = '1'; C := '1'; end loop loop_C; return t; end Create_Table;
Then we simply have to copy-paste the previously tested and grep'ed code.
-
Generalised fault injection
06/07/2019 at 19:40 • 0 commentsChristos has modified the original DFF gates to inject faults and evaluate/validate the fault-tolerant memory cells used by the CERN.
Meanwhile, I created this library to help with ASIC design, which is a different beast and methodology. I have no bit-flip issue (at least, directy) but "Design For Test" requires the design and evaluation of testing methods, such as BIST (at chip power-on) and post-factory pass/fail (for binning). A brand new fabbed chip must be thoroughly tested to ensure ALL the gates and wires work correctly under nominal conditions and a test rig must include all the necessary test vectors.
This is a different fault model, because the changes (usually) don't occur at run time. However the software structure is almost the same, and the fault can be set during startup at random (or under guidance from command line parameters), instead of while simulating.
The faults can be summarized to a simple model : stuck to 0 or stuck to 1. The wires could also be broken, thus float and pick up some neighbouring signal, but it can still be simplified down to : "not getting the expected value".
But how do you change a logic gate ? And how to keep things simple ? And how do we ensure only THAT gate (or a list thereof) is altered ? Well, a good understanding of GHDL and VHDL can do wonders :-D
There are tens of logic gates but they all have the same overall structure : 1, 2 or 3 inputs, then some "computation", then one output.
It's easy to stick the output to 0 or 1 (or even invert it though it's not useful) but it wouldn't be perfectly complete (because there could be faults inside the gate where the changes are conditional) and we can do even better.
The "computation" part can be a performed by a single table lookup. For 3 inputs, the output can be defined by 2^3=8 bits, the description fits in a byte. Each type of gate can thus be implemented as one call to a generic C function in VHPI, with the following parameters :
- A input
- B input
- C input
- lookup table
For now the library implements each gate as a direct boolean computation, it will take some effort to convert all the gates and check them for accuracy but it's not difficult.
The lookup table is usually represented as a string of bits, which is seen as a character string in VHDL. It's not convenient at all so the lookup table needs to be converted to "integer" in a way or another. The gate is easy to alter : simply flip (XOR) one bit of the lookup table.
To prevent conversion problems, and since the gates' functions are already defined and tested, the boolean function can be transformed into an actual VHDL function, that is passed/called by another function, to scan the inputs and create a "generic" during elaboration. This way, we don't have to calculate anything by hand. I'll have to find how to pass a function to another in VHDL... or something like that.
Now we need something else : we need to address each gate, to apply a fault only to one. We need to index/register all the gates to cover, and give each an ID. By default, ID=0 and no fault is applied.
But we also need to locate WHICH gate is faulted, because giving an arbitrary number won't be useful if it can't be matched with the code.
Furthermore, we don't want to have to modify the code and the system MUST register the gates independently, automatically.
Fortunately, VHDL already forces code to include labels to entities. There must be a way to get it, pass it to the generic definition and then to some C code that will perform some filtering.
I will reuse some of the tricks I have implemented for the framebuffer interface, with generics initialised with the return value of a C function that provides new values each time.
It's only the first thoughts and there are still many little details to check and validate. However my experience with GHDL shows that many underlying principles already work :
- it's easy to call a C function, return an int or a std_logic, with arguments types int, std_logic and string. So that's all we need.
- the framebuffer code uses crazy tricks with elaboration-time initialisation of constants and even pointers. Generics and constants are initialised in order so some hacks are possible.
- The code can create efficient simulations if written correctly, for example with all the constants pre-computed during elaboration. The listing of the gates can occur at that moment, filter the appropriate gates with exclude/include arguments...
However so far I have not tried several other things...
- I have not had a design where I needed to track where the call happened because VHPI hooks were always in specific places, not generalised.
- It's only a supposition that the generics will be initialised in sequence. I have to make it work...
- I don't know how to supply a function to another, or something similar, that is defined inside an entity... I can still define the gate function outside the file, in a library, but using OO programming gave me bad habits :-P
OK, enough rants, let's code.
In the INC8 unit, we have lines like this :
e_R7A: entity AND3 port map(A=>A012, B=>A3456, C=>A(7), Y=> V);
Ideally, there is nothing to change, all the code works like before, we just use a different version of the entity.
There should be a way to get the name of the label from within the entity, then pass that string to a C function via VHPI. I'll see if I can use these predefined attributes:
E'SIMPLE_NAME is a string containing the name of entity E. E'INSTANCE_NAME is a string containing the design hierarchy including E. E'PATH_NAME is a string containing the design hierarchy of E to design root.
...
Then the AND3 entity is defined in proasic3/gate_AND3.vhdl by:
entity AND3 is port( A, B, C : in std_logic; Y : out std_logic); end AND3; architecture rtl of AND3 is begin Y <= (A and B) and C after gate_delay; end rtl;
Nothing fancy but all the gates are a variation of this code, with a different truth table, here it would be : "10000000" or 0x80 in short form.
The trick is : how to generate the truth table from within the file, without duplicating too much code ?
entity AND3 is generic( TruthTable : integer := 0x80; EntName : string := something'instance_name ) port( A, B, C : in std_logic; Y : out std_logic); end AND3; architecture rtl of AND3 is begin Y <= VHP_lookup(A, B, C, lookup ) after gate_delay; end rtl;
...
-
Project created on OHWR !
05/11/2019 at 20:02 • 0 commentshttps://ohwr.org/project/microsemi-libIt seems the CERN people are exploiting and even expanding the library. We need to agree on a few conventions to keep all our projects compatible.
A new feature appeared, where errors are injected in flip-flops to test high-reliability redundant designs, using the VHPI interface and external C code. I don't think I'll use this feature because I'm not concerned by rad-hard designs :-)
The gates library might be extended to other FPGA families though it is out of my own scope.
On the one hand, I'm losing some control over the development of the library. On the other, everybody benefits from this consolidation and expansion, where more features are added and cross-tested, and I already have the features I needed initially. The rest is just a bonus ;-)
So that's further proof that, when done correctly, Free Software and Open Source are amazing :-)
-
An unexpected boost
04/11/2019 at 13:43 • 0 commentsIt was a big surprise to receive an email from one of the good folks at CERN.
Even more a surprise when the email says that this library is used for post-synthesis simulation with GHDL !
Even better is the contribution that fills the blanks and adds about 50 missing gates !
It will take a while but the main archive will be updated and will now have much broader use, it will do much more than my own designs but for others who need to replace or even bypass the proprietary tools from Actel/Microsemi/Microchip. GHDL again shows its incredible strengths and now there is a new bridge to the FPGA workflow world !
Many thanks to all the people who make those great contributions to the free and open design tools :-)
-
More gates !
01/04/2019 at 01:39 • 0 commentsI'm adding MAJ3, AXO1 as well as some AX1x gates. The definition of the guide is not consistent with the naming so beware ! AX1 should be (A and B) xor C but the manual says it is AX1C. This should be checked with the actual tools so until then, put warnings everywhere...
Give me some time to update, check and upload the new archive...
I just confirmed that the manual definition matches with the generated bitstream. I dumped the EDIF of some test code and collected the following equations :AX1E "!((A & B) ^ C)" AX1D "!((!A & !B) ^ C)" AX1C "(A & B) ^ C" AX1B "(!A & !B) ^ C" AX1A "!((!A & B) ^ C)" AX1 "(!A & B) ^ C"
So I must update the INC8 code...
-
definition of the gates
12/03/2018 at 05:06 • 0 commentsUpdated 20190328
Updated 20190411 : 50 more gates !
For those who are not familiar with the Actel nomenclature, I have included below the definition of the gates that are implemented so far. This is not exhaustive and I might have omitted many gates because I don't need them (but Christos added more).Of course, some definitions are redundant (by swapping pins or inverting functions and levels: for example NAND2 = OR2C) but they are practical and it is optimised later (with aliases ?).
gate_AND2.vhdl: Y <= A and B after gate_delay; gate_AND2A.vhdl: Y <= (not A) and B after gate_delay; gate_AND3.vhdl: Y <= (A and B) and C after gate_delay; gate_AND3A.vhdl: Y <= ((not A) and B) and C after gate_delay; gate_AND3B.vhdl: Y <= ((not A) and (not B)) and C after gate_delay; gate_AND3C.vhdl: Y <= ((not A) and (not B)) and (not C) after gate_delay; gate_AO1.vhdl: Y <= (A and B) or C after gate_delay; gate_AO12.vhdl: Y <= ((not A) and B) or ((not A) and (not C)) or (B and (not C)) or (A and (not B) and C) after gate_delay; gate_AO13.vhdl: Y <= (A and B) or (A and (not C)) or (B and (not C)) after gate_delay; gate_AO14.vhdl: Y <= (A and B) or (A and (not C)) or (B and (not C)) or ((not A) and (not B) and C) after gate_delay; gate_AO15.vhdl: Y <= (A and (not B) and C) or ((not A) and B and C) or ((not A) and (not B) and (not C)) after gate_delay; gate_AO16.vhdl: Y <= (A and B and (not C)) or ((not A) and (not B) and C) after gate_delay; gate_AO17.vhdl: Y <= (A and B and C) or ((not A) and B and (not C)) or ((not A) and (not B) and C) after gate_delay; gate_AO18.vhdl: Y <= ((not A) and B) or ((not A) and (not C)) or (B and (not C)) after gate_delay; gate_AO1A.vhdl: Y <= ((not A) and B) or C after gate_delay; gate_AO1B.vhdl: Y <= (A and B) or (not C) after gate_delay; gate_AO1C.vhdl: Y <= ((not A) and B) or (not C) after gate_delay; gate_AO1D.vhdl: Y <= ((not A) and (not B)) or C after gate_delay; gate_AO1E.vhdl: Y <= ((not A) and (not B)) or (not C) after gate_delay; gate_AOI1.vhdl: Y <= not ( (A and B) or C ) after gate_delay; gate_AOI1A.vhdl: Y <= not ( ((not A) and B) or C ) after gate_delay; gate_AOI1B.vhdl: Y <= not ( (A and B) or (not C) ) after gate_delay; gate_AOI1C.vhdl: Y <= not ( ((not A) and (not B)) or C ) after gate_delay; gate_AOI1D.vhdl: Y <= not ( ((not A) and (not B)) or (not C) ) after gate_delay; gate_AOI5.vhdl: Y <= not ( ((not A) and B and C) or (A and (not B) and (not C)) ) after gate_delay; gate_AX1.vhdl: Y <= ((not A) and B) xor C after gate_delay; -- /!\ gate_AX1A.vhdl: Y <= not (((not A) and B) xor C) after gate_delay; -- beware of definition and inversion ! gate_AX1B.vhdl: Y <= ((not A) and (not B)) xor C after gate_delay; -- beware of definition ! gate_AX1C.vhdl: Y <= (A and B) xor C after gate_delay; -- /!\ mistaken with AX1 in the guide ? gate_AX1D.vhdl: Y <= not(((not A) and (not B)) xor C) after gate_delay; gate_AX1E.vhdl: Y <= not((A and B) xor C) after gate_delay; gate_AXO1.vhdl: Y <= (A and B) or (B xor C) after gate_delay; gate_AXO2.vhdl: Y <= ((not A) and B) or (B xor C) after gate_delay; gate_AXO3.vhdl: Y <= (A and (not B)) or (B xor C) after gate_delay; gate_AXO5.vhdl: Y <= ((not A) and B) or ((not B) xor C) after gate_delay; gate_AXO6.vhdl: Y <= (A and (not B)) or ((not B) xor C) after gate_delay; gate_AXO7.vhdl: Y <= ((not A) and (not B)) or (B xor C) after gate_delay; gate_AXOI1.vhdl: Y <= not ((A and B) or (B xor C)) after gate_delay; gate_AXOI2.vhdl: Y <= not (((not A) and B) or (B xor C)) after gate_delay; gate_AXOI3.vhdl: Y <= not ((A and (not B)) or (B xor C)) after gate_delay; gate_AXOI4.vhdl: Y <= not ((A and B) or ((not B) xor C)) after gate_delay; gate_AXOI5.vhdl: Y <= not (((not A) and B) or ((not B) xor C)) after gate_delay; gate_AXOI7.vhdl: Y <= not (((not A) and (not B)) or (B xor C)) after gate_delay; gate_CLKINT.vhdl: Y <= A after gate_delay; (just a buffer) gate_DFN1.vhdl: Q<=D after gate_delay; (always enabled) gate_DFN1C0.vhdl: Q<=D after gate_delay; gate_DFN1C1.vhdl: Q<=D after gate_delay; gate_DFN1E0.vhdl: Q<=D after gate_delay; gate_DFN1E0C0.vhdl: Q<=D after gate_delay; gate_DFN1E0C1.vhdl: Q<=D after gate_delay; gate_DFN1E0P0.vhdl: Q<=D after gate_delay; gate_DFN1E0P1.vhdl: Q<=D after gate_delay; gate_DFN1E1.vhdl: Q<=D after gate_delay; (enable when E=1, no clear) gate_DFN1E1C0.vhdl: Q<=D after gate_delay; (enable when E=1, clear when C=0) gate_DFN1E1C1.vhdl: Q<=D after gate_delay; (enable when E=1, clear when C=1) => bug ! gate_DFN1E1P0.vhdl: Q<=D after gate_delay; gate_DFN1E1P1.vhdl: Q<=D after gate_delay; gate_DFN1P0.vhdl: Q<=D after gate_delay; gate_DFN1P1.vhdl: Q<=D after gate_delay; gate_INV.vhdl: Y <= not(A) after gate_delay; gate_MAJ3.vhdl: Y <= (A and B) or (A and C) or (B and C) after gate_delay; gate_MX2.vhdl: Y <= A after gate_delay when S='0' else B after gate_delay; gate_MX2A.vhdl: Y <= (not A) after gate_delay when S='0' else B after gate_delay; gate_MX2B.vhdl: Y <= A after gate_delay when S='0' else (not B) after gate_delay; gate_MX2C.vhdl: Y <= (not A) after gate_delay when S='0' else (not B) after gate_delay; gate_NAND2.vhdl: Y <= not (A and B) after gate_delay; gate_NAND3A.vhdl: Y <= not (((not A) and B) and C) after gate_delay; gate_NAND3B.vhdl: Y <= not (((not A) and (not B)) and C) after gate_delay; gate_NAND3.vhdl: Y <= not ((A and B) and C) after gate_delay; gate_NOR2A.vhdl: Y <= not((not A) or B) after gate_delay; gate_NOR2.vhdl: Y <= A nor B after gate_delay; gate_NOR2B.vhdl: Y <= not((not A) or (not B)) after gate_delay; gate_NOR3.vhdl: Y <= not ((A or B) or C) after gate_delay; gate_NOR3A.vhdl: Y <= not ((not A) or B or C) after gate_delay; gate_NOR3B.vhdl: Y <= not ((not A) or (not B) or C) after gate_delay; gate_NOR3C.vhdl: Y <= not ((not A) or (not B) or (not C)) after gate_delay; gate_OA1A.vhdl: Y <= ((not A) or B) and C after gate_delay; gate_OA1B.vhdl: Y <= (A or B) and (not C) after gate_delay; gate_OA1C.vhdl: Y <= ((not A) or B) and (not C) after gate_delay; gate_OAI1.vhdl: Y <= not ((A or B) and C) after gate_delay; gate_OR2.vhdl: Y <= A or B after gate_delay; gate_OR2A.vhdl: Y <= (not A) or B after gate_delay; gate_OR2B.vhdl: Y <= (not A) or (not B) after gate_delay; gate_OR3.vhdl: Y <= (A or B) or C after gate_delay; gate_OR3A.vhdl: Y <= ((not A) or B) or C after gate_delay; gate_OR3B.vhdl: Y <= ((not A) or (not B)) or C after gate_delay; gate_OR3C.vhdl: Y <= ((not A) or (not B) or (not C)) after gate_delay; gate_XA1.vhdl: Y <= (A xor B) and C after gate_delay; gate_XA1A.vhdl: Y <= (not (A xor B)) and C after gate_delay; gate_XA1B.vhdl: Y <= (A xor B) and (not C) after gate_delay; gate_XA1C.vhdl: Y <= (not (A xor B)) and (not C) after gate_delay; gate_XNOR2.vhdl: Y <= not (A xor B) after gate_delay; gate_XNOR3.vhdl: Y <= not ((A xor B) xor C) after gate_delay; gate_XO1.vhdl: Y <= (A xor B) or C after gate_delay; gate_XO1A.vhdl: Y <= (not (A xor B)) or C after gate_delay; gate_XOR2.vhdl: Y <= A xor B after gate_delay; gate_XOR3.vhdl: Y <= (A xor B) xor C after gate_delay; gate_ZOR3.vhdl: Y <= (A and B and C) or ((not A) and (not B) and (not C)) after gate_delay; gate_ZOR3I.vhdl: Y <= not ((A and B and C) or ((not A) and (not B) and (not C))) after gate_delay;
You will recognize the basic logic gates : AND, OR, XOR, MUX and their variants with inverted inputs or outputs (XNOR, NOR, NAND).
The letter at the end describes how many inputs are inverted (useful for bubble-pushing). A=1 inverted input, B=2, C=3... (though the rule seems broken in the AXx gates, double-check the definitions !)
Composite gates use single letters : A=AND, O=OR, X=XOR, I=Inverter
As far as I can remember, this nomenclature dates back to the Actel ACT1 family (late 80s), I remember looking at thick printed manuals to get the right gate for a design that I wanted to fit in the A1020 chip, back in 1997 in school :-) This has been ported from the antifuse to the flash devices and it's another reason why I'm familiar with the A3P family.