Understanding the simplified computations of publicly available implementations of TMDS encoding — My journey with the Amaranth HDL
Where I explain how the head scratching specifications are greatly simplified in actual code…
[This is a part of my series about Amaranth HDL.]
One of my long term project with HDLs requires a screen display, and since I got a development board having an HDMI compatible output connector, I naturally tried to take advantage of this. For the record, my fallback plan would have been to use lots of GPIOs and a R2R ladder to be linked to a VGA connector.
Finding and studying reference material
Looking for some reference material, I quickly found this tutorial : «Tutorial 6: HDMI Display Output», by Jeremy See, which is a very nice article, by the way. And this tutorial also referenced the HDMI presentation and sample code by fpga4fun. So if you want to know about this topic, please read those pages.
By the way, what is publicly available is the DVI 1.0 specifications. HDMI specifications, as well as latest DVI specification, requires to pay thousands of dollars of membership fees each year, so no thanks, I’m a poor hobbyist.
So, I avidly read how all of that was working, especially the TMDS encoding. That’s how something quickly puzzled me, as I tried to reconcile the sample code with the described algorithm.
Because, the official specification describe tests and computations like in this extract :
…Where the corresponding sample code looks more like that (comments by me) :
// as Cnt(T-1)
reg [3:0] balance_acc = 0;
// instead of "number of 1-bits" minus "number of 0-bits" --> what ?
wire [3:0] balance = q_m[0] + q_m[1] + q_m[2] + q_m[3] + q_m[4] + q_m[5] + q_m[6] + q_m[7] - 4'd4;
// Compare the signs
wire balance_sign_eq = (balance[3] == balance_acc[3]);
// Compute the result --> where is the "×2" ?
wire invert_q_m = (balance==0 || balance_acc==0) ? ~q_m[8] : balance_sign_eq;
wire [3:0] balance_acc_inc = balance - ({q_m[8] ^ ~balance_sign_eq} & ~(balance==0 || balance_acc==0));
wire [3:0] balance_acc_new = invert_q_m ? balance_acc-balance_acc_inc : balance_acc+balance_acc_inc;
wire [9:0] TMDS_data = {invert_q_m, q_m[8], q_m[7:0] ^ {8{invert_q_m}}};
Understanding how the code ended up simplified like that
First simplification
Let’s start with the simplification of the comparison between the number of ones (let’s call it N1) and the number of zeros (let’s call it N0) in a byte.
A byte is made of 8 bits, thus we have N0 + N1 = 8 ; meaning that we have N0 = 8 - N1.
Thus, testing whether N1 is greater than N0, meaning N1 - N0 > 0, becomes testing whether N1 - (8 - N1) > 0, that becomes 2 × N1 - 8 > 0, then
2 × (N1–4) > 0, in other words N1 - 4 > 0. That’s the computation of balance in the previous code excerpt.
Second simplification
Now let’s see how the computation of Cnt(t) a.k.a. balance_acc in the code, is simplified.
As a sidenote, the actual sign of the computation depends on whether XOR or XNOR was used to minimise transition just before caring about the balance of ones and zeros.
When the balance is zero, like at the beginning, then the next value of balance is ±(N1-N0), that is to say ±2×(N1–4), in other words the next value of balance is an even number.
When the balance is a non-zero even number (let’s write it 2×K), then the next value of balance is 2×K ± 2×(0 or 1) ±(N1 - N0). We previously saw that
N1 - N0 becomes 2×(N1 – 4), thus the next value is 2×K ± 2×(0 or 1) ±2×(N1–4), that can be written 2×(K ± (0 or 1) ± (N1–4)), in other words the next value of balance is an even number.
That explains that we just compute the half balance, and N1–4 has already been computed by the way, as balance in the code.
Conclusion
Once I understood how the simplification happened, it was time to write actual code.
Wait a minute ! How do we verify that the encoding is correctly implemented ? That will a topic for the next time…