The Gardner Timing Error Detector (TED) when at zero error positions the samples as follows related to the equation for the timing error:
Specific to the use of the Gardner TED for higher order QAM I offer the following from my own experience in using it successfully for this purpose:
First we note that the Gardner TED can be viewed as a form of the Maximum Likelihood Timing Detector which for complex signals takes the general expression as:
$$\epsilon = \text{Real}\{\dot y \cdot y^* \}$$
Where $\epsilon$ is an error term that for small timing offsets is approximately proportional to the timing error, $y$ is the sample at the ideal time location with zero time error, $\dot y$ is the derivative of $y$ and $y^*$ is the complex conjugate of $y$.
Note intuitively what is happening here in vicinity of the desired lock-point (tracking condition): Consider $\dot y$ as a sign and a scaling of the transition between symbols as given by $y^*$. The TED is using the transition from one symbol to the next and changing it's sign and scaling it accordingly to create the "S-curve" for error discrimination on average. If the real (or imaginary) transition is going from a lower value to a higher value, the TED will pass that through directly (but reduce it's level for shorter transitions, which is consistent with optimum ratio combining, allowing the larger transitions which have a higher SNR with regards to time error detection to provide a bigger contribution toward the result), while if the transition is going from a higher value to a lower value, the TED with invert the sign and then pass that through as the error curve. The variation we see on any given transition is the "pattern noise" inherent in the Gardner TED, which in practical application will be filtered out below concern by the timing error tracking loop (but which is why I suggest to use the Gardner TED before final RRC matched filtering in the receiver as detailed here).
For the Gardner TED specifically, the derivative $\dot y$ is approximated using $y_{n+1}-y_{n-1}$ and we get:
$$TED = \text{Real}\{(y_{n+1}-y_{n-1}) \cdot y_n^* \}$$
Which in using $y=I+jQ$ reduces nicely to:
$$TED = I_n(I_{n+1}-I_{n-1})+Q_n(Q_{n+1}-Q_{n-1})$$
Where $y_{n}$ refers to what would be the sample between symbols when error is 0, and $y_{n+1}$ and $y_{n-1}$ the samples adjacent on each side, with the sampling rate as two samples per symbol as commonly done with the Gardner TED.
Below is a demonstration confirming that this complex form of the Gardner TED works well for higher order modulations, in this case 16-QAM specifically.
The eye diagram of the waveform, in this case with a timing offset (actual samples at 4 samples/symbol are shown as bloe dots) is shown in the plot below (showing the real portion while the imaginary portion would look similar).
My Python code for the Gardner TED is given below;
def ted(tx, n, offset):
'''
tx: oversampled complex waveform
n: oversampling rate
offset: sample offset delay
'''
# downsample to 2 samples per symbol with timing offset
tx2 = tx[offset::int(n/2)]
# generate a prompt, late and early each offset by 1 sample
late = tx2[2:]
early = tx2[:-2]
prompt = tx2[1:-1]
# compute and return the Garnder Error result
return np.real(np.conj(prompt[::2])*(late-early)[::2])
In order to confirm the average has the S-curve associated with the Gardner TED, I rotated through all possible offsets and computed the average for each offset to generate the error curve as given below:
(As a side note, I have completed the same test showing how the M&M timing error detector can also be used for QAM).
This works as long as the data is generally equiprobable (as is typically the case especially when data scrambling is utilized) so on average the expected result is achieved. Thus even though not every symbol transitions through zero, for every non-zero transition there is an identical non-zero transition of opposite sign given the equiprobably distribution, resulting in a zero average when no timing offset exists.
I explain in more detail how the Gardner TED works where the timing loop used would be concerned with the average and not the error update after any given symbol transition, and how this would apply to BPSK, QPSK, and QAM waveforms at these additional posts:
Gardner Timing Recovery for Repeated Symbols
Isn't Gardner's algorithm and Early-Late gate the same thing?