<jupyter_start><jupyter_text>Automatic Gain Control with PYNQWe present a digital Automatic Gain Control (AGC) circuit with interactive control of it's parameters. This is a purely digital loopback system, so no extra hardware is required. We'll generate various input signals, explore some interesting effects of the AGC algorithm, and practice tweaking our parameters for best performance.The design of this AGC example will be explored, featuring various hardware arithmetic approximations for power estimation, logarithms and exponentiation. Aims* Interactively explore the behaviour of an AGC circuit* Learn to tune the AGC parameters for different scenarios* Explore our open-source hardware design, with a focus on arithmetic approximations Table of Contents* [1. Introduction](1.-Introduction)* [2. ...And we're off!](go)* [3. Exploring some scenarios](3.-Exploring-some-scenarios)* [4. A closer look at the hardware](hw)    * [4.1 Power estimation](power-est)    * [4.2 Logarithms and exponentials](log-est)* [5. Conclusion](conclusion)----  1. IntroductionThis example is a purely digital loopback, so there is no requirement for extra hardware. If you've got a Pynq Z2 board, ZCU111, or RFSoC 2x2, you can run it and have a play with all of the interactive features. Let's take a look at how this demonstration is structured.        Figure 1: System overview The datapath is a simple loopback and we'll generally be following only a few steps:  * Generate a signal in software  * Stream our input signal _to_ the AGC circuit, via the first DMA  * Stream our output signal _from_ the AGC, via the second DMA  * Inspect the output signal via this notebookWe should also note that there are a few parameters for the AGC circuit, which we can set from software as well. Just before we start interacting with our AGC circuit, let's take a moment to look at it's design and appreciate what its behaviour should be. Our circuit is loosely based on the algorithm presented in [this book](https://www.elsevier.com/books/rf-and-digital-signal-processing-for-software-defined-radio/rouphael/978-0-7506-8210-7)[1] (and there's a free excerpt from the AGC section [online](https://www.edn.com/wireless-101-automatic-gain-control-agc/)[2]). Why use AGC?The main goal of the AGC circuit is to keep a signal's average power steady, at a known level. This is used to mitigate fluctuations in a signal's received power. In the analogue world, this is often to ensure that we present a full-scale signal to our ADC to minimise the impact of quantisation errors. We'll show the use of such an external analogue gain with RFSoC in a future notebook. However, there are quite a few uses in the digital world too --- generally when we want to normalise signals digitally at IF or baseband. Synchronisation for QAM receivers is one example. The synchronisation task is much simpler if we have a priori knowledge of the signal's average power, since we know what amplitude to expect for each point in the constellation. We can use an AGC, just like this one, to ensure our synchronisation circuit is given a signal with it's preferred average power. This can not only "correct" the received signal's power, but also adjust for any changes over time. Our DesignOur AGC circuit is a feedback loop that will amplify our signal, aiming to match a reference average power. There are 3 main parameters we will be tuning from software: the desired average power (ref), the number of samples used to calculate the average power (window), and how aggressively we want to vary our gain (alpha).        Figure 2: AGC block diagramOur control is based on the _logarithm_ of the input power... and these $log_{10}(x)$ and $10^{x}$ blocks _really_ complicate the digital design. A much simpler AGC design could respond linearly (instead of logarithmically) but this would have slightly different behaviour. We opt for the more painful route for two reasons:  * The log response lets us react to large abrupt changes to the input signal roughly as quickly as we can to small changes. This is a nice property to have when dealing with packets or bursts of data. It also simplifies any analysis if we can assume that the "recovery time" is independent of the input signal.    * It will make it easier to interface to analogue Variable Gain Amplifiers (VGAs) in the future. Most  VGAs offer a control for their gain that is "linear in dB" (i.e. it's logarithmic!). By having our control loop already using logarithms, we could have direct linear control of a VGA without extra interfacing circuitry.Let's now explore the behaviour of our system while getting our hands dirty 2. ...And we're off! Run the following code cell to launch our GUI. This lets you set AGC parameters and generate your own input signals. The input signals will automatically be streamed through the AGC circuit and visualised below. You can control the envelope of the input signal by clicking and dragging the "envelope handles" (the blue circles). The window parameter (how many samples are averaged during our power calculation) is shown on the output plot as a grey area at the far left.Check [this animation](https://github.com/strath-sdr/pynq_agc/blob/master/demonstration.gif) to see an example of setting the envelope.<jupyter_code>import pynq_agc

model = pynq_agc.AgcDashModel(fs=int(1e6), N=6000)
app = pynq_agc.AgcDashController(model)
app.show()<jupyter_output><empty_output><jupyter_text>We'd recommend having a play with different envelopes, signal types, and AGC parameters to get an intuition for how the AGC behaves. Once you've done some exploring, we present a couple of (somewhat) realistic scenarios and discuss them below. 3. Exploring some scenariosHere are a few examples of configurations which demonstrate real-world communications challenges related to AGC.You can select each scenario the with "Preset Scenarios" input at the bottom of the GUI.>  Slow Fading> Here we have an effect that emulates "slow fading" of a simple signal. We'd usually expect this to be caused by movement, often with buildings or terrain impeding the signal. If the receiver is sensitive to the amplitude of the received signal (e.g. AM or many QAM synchronisers), we'd need use an AGC to compensate for the changes in signal strength.>> Try different window and alpha parameters to see how constant you can keep the output power. Notice that increasing the window size will reduce the time resolution of our gain control, which leads to a noticeable saw-tooth envelope for slow fading effects. If alpha is too high, we can see the output ringing where we've overshot by adjusting the gain too aggressively. If alpha is too low, we might not be compensating quickly enough for the fading effects.>  AM Envelope Preservation> AM demodulation is a classic AGC example. For AM radio receivers, we want to play baseband signals with a fixed average power so broadcasts from different locations are all audible (and the more powerful ones aren't deafening!). Our information is encoded in the envelope of the signal and we need to be very careful not to distort it. If our AGC reacts too quickly, it can start to smooth out the envelope, destroying our information. Try reducing the window until this starts to happen. The window length is shown as the grey rectangle on the output plot. Try to achieve the same effect by decreasing the modulated "Data" frequency as well.>  Packet Preambles with QPSK> Here we present a bursty QPSK transmission, as if receiving a single packet.> We can clearly see the recovery time the AGC needs in order to converge to a steady gain. Having this steady, known power can be important for synchronisation (especially for higher order QAMs) and we can change this level using the ref parameter. However the AGC's recovery time means that the first few symbols will likely not be received correctly. In this situation we need to tune our AGC parameters to ensure we'll reach equilibrium before the packet's preamble is finished (or vice versa). We can tune our alpha, ref, and window parameters and then deduce the minimum preamble length that would be appropriate for this packet format. Just like our AM scenario, QPSK and QAM signals will start to become distorted if our window is small relative to the symbol period.The rest of the notebook will address our AGC's hardware design in a little more depth. If you only want to use the AGC and aren't interested in the FPGA design, feel free to stop here. 4. A closer look at the hardware This section will give some insights into the FPGA design for our AGC and we'll focus on the fixed-point arithmetic techniques used to implement the more exotic functions such as power estimation, logarithms, exponentiation. The full source code for the circuit is available on [our GitHub](https://github.com/strath-sdr/pynq_agc) and is written in the excellent functional language, [Clash](https://clash-lang.org/). First of all, let's revisit our block diagram.        Figure 3: Revisiting the AGC block diagramThis model presents quite a concise mathematical system. Drawing from the loop analysis presented by [<NAME>](https://www.elsevier.com/books/rf-and-digital-signal-processing-for-software-defined-radio/rouphael/978-0-7506-8210-7), we can formally represent this model with a few manageable equations. Our averaged input power, $P_{in}$, is at the output of our integrate-and-dump block. Given inputs $I_b(n)$, $Q_b(n)$, and our window parameter $N$, we define $P_{in}(n)$ as: $$ P_{in}(n) = \frac{1}{N} \sum_{k=0}^{N-1}\sqrt{ I_b^2(n-k) + Q_b^2(n-k) } $$We also need to find an equation for the output of our accumulator, $v(n)$. Given our alpha and ref parameters, $\alpha$ and $P_{ref}$, we can write an expression for $v(n)$ in state-space representation:$$ v(n+1) = v(n) + \alpha [P_{ref} - \log_{10}(10^{v(n)} P_{in}(n))] $$And finally to close the loop, we can model the outputs of our gain stage, $I_b(n)$ and $Q_b(n)$, in terms of the input signal, $I_a(n)$ and $Q_a(n)$, and $v(n)$.$$ I_b(n) = 10^{v(n)}I_a(n) \\ Q_b(n) = 10^{v(n)}Q_a(n) $$Although the _analysis_ of this system can be quite complex, it is at least simple to describe using this state-space form. In fact, we could translate these equations to a software implementation with very little effort.<jupyter_code>import numpy as np

def python_agc(i_a, q_a, p_ref, alpha, N):
    
    v = 0
    i_b = np.zeros((len(i_a),))
    q_b = np.zeros((len(q_a),))
    
    for (n,(i,q)) in enumerate(zip(i_a,q_a)):
            i_b[n] = (10**v * i)
            q_b[n] = (10**v * q)
    
            # Protect against averaging empty lists           vvvvvvvvvvvvvv
            p_in = np.average( np.abs(i_b + 1j*q_b)[n-N:n]    if n>=N else 0 )
              
            # Protect against log10(0) for later              vvvvvvvvvvvvvvvvvvvvvv
            inner_term = 10**v * p_in                         if p_in > 0 else 1e-12
                
            v = v + alpha * (p_ref - np.log10(inner_term))
            
    return (i_b, q_b)<jupyter_output><empty_output><jupyter_text>So, if a software implementation (albeit a _very_ slow one) is so easy to implement, what is the challenge in translating this to hardware? Of course the standard answers apply here, such as complexity introduced by implementing AXI-Stream / DataFlow control, fixed-point conversion, pipelining, etc. In this particular case, though, there are two interesting challenges: power estimation and logarithms/exponentials.  4.1. Power estimation Our power estimation step is presented with a complex signal and needs to compute it's magnitude:$$ P_{in}(n) = \sqrt{ I_b^2(n) + Q_b^2(n) } $$First of all, we could implement the $x^2$ function as a single multiply but we might want to avoid this if the wordlengths exceed that of our DSP48 slice (27x18 for RFSoC or 25x18 for Pynq-Z2). For RFSoC devices our I/Q values will be 16 bit and we immediately multiply it by our gain. Even a small gain would take our signal beyond the 18 bit limit, so each $x^2$ operation would consume 4 DSP48 slices! Beyond this, we still face the challenge of implementing the $\sqrt{x}$ function. This is much harder to implement directly with our standard set of arithmetic features. We could do this using a technique such as CORDIC (presented in the [next section](4.2.-Logarithms-and-exponentials)) but since the multiplications are also a little awkward to implement, let's consider an approximation for the whole function.Stepping back from circuit design for a moment, one common mathematical approximation for our power estimation is:$$ P_{in}(n) \approx \alpha \cdot \text{max} \{ I_b(n),Q_b(n) \} + \beta \cdot\text{min} \{ I_b(n),Q_b(n) \} $$This somewhat surprising approximation reduces the power estimation to one comparison (to find the min and max of our inputs), one addition, and two multiplications. This is a very good starting point, but we can avoid even the multiplications if we intelligently select values for our constants, $\alpha$ and $\beta$. Some suggested values are listed by a lovely little bit of software by [<NAME>](https://dspguru.com/dsp/tricks/magnitude-estimator/)[3]. We opt for $\alpha = \frac{61}{64}$ and $\beta = \frac{13}{32}$.Multiplications by powers of 2 can clearly be replaced by shifts (implemented for free in the routing). In order to implement our chosen $\alpha$ and $\beta$ constants, we can construct non-power of 2 multiplications by adding/subtracting powers of 2 or any other intermediate values. We can model these constant multiplications as a graph of shifts and additions/subtractions. We could probably construct this graph intuitively for simple examples but we can rely on software tools such as the [SPIRAL project](http://spiral.ece.cmu.edu/mcm/gen.html) to construct graphs for more awkward coefficients[4]. In our case, our constant multiplication circuits become:        Figure 4: Implementing constant multiplications In summary, we can use an approximation to reduce our power estimation to the sum of two multiplications. We can select our coefficients wisely to further reduce our circuit to a graph of shifts and additions/subtractions --- in our case, each of our multiplications require only two additions. Of course this is just an _approximation_ of the actual power. Our coefficients produce an average error of less than $1\%$ and a worst case error of $4.7\%$; a decent balance between implementation cost and accuracy. 4.2. Logarithms and exponentials We explored two methods of implementing logarithms and exponentials --- one single-cycle and one iterative implementation. A single-cycle approach was appealing in the context of this AGC because we'd like to avoid introducing extra latency into our feedback loop. The longer our latency, the greater the potential for our control signal to "ring" or oscillate around our desired value. Unfortunately this approach requires FPGA resources that grow exponentially with the desired precision. This worked very nicely for wordlengths around 16 bits, but our full 26 bit words would pose a serious challenge. While our final design uses the second approach, both are interesting to discuss. Intuitive  single-cycle logarithmSince we're working with digital logic, let's start by looking at base 2 only. We're working towards designing a circuit to perform $\log_2(x)$ and $2^x$. This approach uses one mathematical trick and then brute-forces the rest with a precomputed look-up table. It resembles the solution proposed by [this Beyond Circuits blog post](https://www.beyond-circuits.com/wordpress/2015/10/computing-the-logarithm-base-2/)[5].There is one trick that applies to unsigned numbers greater than 0. Here the index of the most significant '1' bit in a binary word is equal to the integer part of our base 2 logarithm. We can demonstrate this just by considering the information we have about a number's range given the position of its most significant '1'.For any binary number, $x$, whose most significant one is at bit index $b$:$$2^b \leq x \lt 2^{b+1}$$This inequality holds also for the logarithm of x:$$\begin{align}\log_2(2^b) &\leq \log_2(x) \lt \log_2(2^{b+1}) \\b &\leq \log_2(x) \lt b+1\end{align}$$This tells us that, since $b$ is an integer, $\lfloor \log_2(x) \rfloor = b$. Equipped with this observation we can implement a full fixed-point logarithm circuit by adding a look-up table for the fractional bits. We'll feed the $N$ bits immediately following the most significant '1' into the address lines of this look-up table, returning our precomputed result for $log_2(1+2^{-N}\text{addr})$.        Figure 5: Example of LUT-based logarithm The final step is to change the base of the logarithm to 10. We can use the following identity to do this with a multiplication by a constant.$$\begin{align}\log_{10}(x) &= \frac{\log_2(x)}{\log_2(10)} \\~\\  &\approx 0.301 \log_2(x)\end{align}$$Just be mindful of your output wordlengths in order to fit this multiplication in a single DSP48 slice. The main issue for our design is the size of the LUT required --- it needs $2^N$ entries to perform the logarithm over $N+1$ input bits. Let's now take a look at our second method, which instead sacrifices some extra clock cycles to keep our circuit slim. Hyperbolic CORDICOur second implementation is based on <NAME>'s 1959(!) Coordinate Rotation DIgital Computer, or [CORDIC](https://dl.acm.org/doi/10.1145/1457838.1457886)[6], and <NAME>'s [insights from 1971](https://dl.acm.org/doi/10.1145/1478786.1478840) which unified many similar algorithms[7]. Let's begin by moving the goalposts and discussing how we can solve trigonometric functions in digital logic.Although we are very used to seeing $\sin(\theta)$ plotted as a continuous wave against a time-axis, we can also think of it as a rotation of a unit vector by $\theta$ radians. This has been very nicely animated by a Desmos user (unfortunately anonymously), embedded below.<jupyter_code>%%html
<iframe src="https://www.desmos.com/calculator/sysjowight?embed" width="1000px" height="250px" frameborder=0></iframe><jupyter_output><empty_output><jupyter_text>The heart of Volder's observation is that we can very efficiently rotate a vector by any angle of the form $\pm\tan^{-1}(2^{-n})$, albeit with a slight change in magnitude. As with our multiplications by a constant in the previous section, shifts and adds are the key to keeping our circuit small. Volder noted that we can rotate a vector by simply incrementing the $x$ coordinate by a power-of-two fraction (a right shift) of the $y$ coordinate and vice versa. This means we only need two additions and a shift for each rotation. The main disadvantage of this simple rotation is that the magnitude of the vector is slightly distorted, but we can correct for this with a constant multiplication or wisely selecting our input values.Our rotation of a vector, $(x_i,y_i)$, by an angle of $\tan^{-1}(2^{-n})$ radians is formally shown below.$$x_{i+1} = x_i + 2^{-n}y_i  \\y_{i+1} = y_i - 2^{-n}x_i$$To rotate in the other direction, we just need to the swap the signs of the second terms. Why this simple expression results in a rotation might not be immediately obvious, so let's explore this using our own Desmos animation. We show the original vector in purple and the rotated vector in orange. We can clearly see a change in angle and a modest change in length. We can directly calculate the length of the green vector (which is perpendicular to the original vector) and use a little bit of trigonometry to find the angle of rotation.<jupyter_code>%%html
<iframe src="https://www.desmos.com/calculator/bel45oxxpn?embed" width="1000px" height="400px" frameborder=0></iframe><jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.06921555702043507, 'fraction_numerical': 0.013792404036306475, 'mean_word_length': 4.409215578716402, 'pattern_counts': {'":': 0, '<': 25, '<?xml version=': 0, '>': 36, 'https://': 12, 'lorem ipsum': 0, 'www.': 6, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 2, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '4', 'dir': 'jupyter-structured-clean-dedup', 'id': '291135', 'license': 'BSD-3-Clause', 'n_tokens_mistral': 5360, 'n_tokens_neox': 4988, 'n_words': 2867, 'path': 'notebooks/AGC_Dashboard.ipynb', 'repo_name': 'jogomojo/pynq_agc', 'size': '19749'}