[{"content":"Overview #Designing an FIR Filter in Matlab and implementing in C++ and VHDL to perform optimization. The filter parameters were:\nSampling Rate: 100 MHz Passband: 10 MHz Stopband: 15 MHz Theoretical Design #The filter was first designed in matlab to determine the minimum amount of taps, and to find the corresponding FIR coefficients.\nrp = 3; rs = 40; fs = 100e6; Fpass = 10e6; Fstop = 17e6; dev = [(10^(rp/20)-1)/(10^(rp/20)+1) 10^(-rs/20)]; [n, fo, ao , w]=firpmord([Fpass Fstop],[1 0],dev,fs); b = firpm(n,fo,ao,w); freqz(b,1,1024,fs) fprintf(\u0026#39;b(%d) = %.6f\\n\u0026#39;, [1:length(b); b]); fprintf(\u0026#39;%.6f\\n\u0026#39;,b\u0026#39;); The response of the theoretical filter can be seen below:\nImplementation #Using the 16 FIR coefficients from the Parks-McClellan optimal FIR filter order estimation, I could now move on to translating the math into floating point arithmetic. This was done by implementing the convolution equation: $$y(n)=\\sum^{M-1}_{k=0}h(k)*x(n-k)$$C++ Design #This was implemented in C++ using a circular buffer\nclass FIR { private: std::vector\u0026lt;double\u0026gt; h; std::vector\u0026lt;double\u0026gt; x; size_t n; size_t M; public: FIR(const std::vector\u0026lt;double\u0026gt;\u0026amp; coeffs) : h(coeffs), x(coeffs.size(), 0.0), n(0), M(coeffs.size()) {} double process(double sample){ x[n]=sample; double y_n = 0.0; size_t idx = n; for (size_t k = 0; k\u0026lt;M; k++){ y_n += h[k]*x[idx]; idx = (idx==0) ? M - 1 : idx-1; } n++; if (n==M) n=0; return y_n; } }; VHDL Design #","date":"1 August 2026","permalink":"https://scast3.github.io/projects/filter-design/","section":"Projects","summary":"\u003ch2 id=\"overview\" class=\"relative group\"\u003eOverview \u003cspan class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100\"\u003e\u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\" style=\"text-decoration-line: none !important;\" href=\"#overview\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\u003c/span\u003e\u003c/h2\u003e\u003cp\u003eDesigning an FIR Filter in Matlab and implementing in C++ and VHDL to perform optimization. The filter parameters were:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eSampling Rate: 100 MHz\u003c/li\u003e\n\u003cli\u003ePassband: 10 MHz\u003c/li\u003e\n\u003cli\u003eStopband: 15 MHz\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"theoretical-design\" class=\"relative group\"\u003eTheoretical Design \u003cspan class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100\"\u003e\u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\" style=\"text-decoration-line: none !important;\" href=\"#theoretical-design\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\u003c/span\u003e\u003c/h2\u003e\u003cp\u003eThe filter was first designed in matlab to determine the minimum amount of taps, and to find the corresponding FIR coefficients.\u003c/p\u003e","title":"FIR Filter Design/Implementation"},{"content":"Overview #This project was the design, simulation, and analysis of a circularly polarized (CP) microstrip patch antenna operating at a center frequency of 8.75 GHz, along with a 4-element array configuration.\nCircular polarization is achieved through a corner-chamfered patch fed by a coaxial probe beneath the ground plane offset from the edge. The single element design is first validated against all specifications before a 4-element array is investigated at element spacings of $0.5\\lambda$, $0.75\\lambda$, and $\\lambda$ to evaluate mutual coupling and radiation performance. All simulations were performed in Ansys HFSS. Read full design and fabrication report.\nDesign targets:\nParameter Requirement Center frequency 8.75 GHz Bandwidth (S11 \u0026lt; -10 dB) 8.5 – 9.0 GHz Axial ratio at broadside \u0026lt; 3 dB RHCP/LHCP gain difference ≥ 15 dB Antenna Design #In this design, a Rogers RO3003 substrate was used with a relative permittivity of $\\varepsilon_r = 3.0$ and a thickness of $h = 1.52$ mm.\nPatch Dimensions #Before simulation, the theoretical dimensions for the patch antenna were calculated using closed-form design equations. In this design, a Rogers RO3003 substrate was used with a relative permittivity of $\\varepsilon_r = 3.0$ and a thickness of $h = 1.52$ mm.\nA square patch geometry is required to support the two orthogonal modes needed for circular polarization. The effective dielectric constant and fringing field extension were calculated analytically, giving a starting patch size of 9.11 × 9.11 mm.\nThe coaxial feed offset for a 50 Ω match was estimated at $y_0 \\approx$ 2.84 mm from the edge.\nCircular Polarization via Corner Chamfer #To achieve circular polarization using a single probe feed, two opposite corners of the square patch are truncated by a length $dL$. The truncation length is related to the quality factor $Q_0$:\n$$ dL = L \\sqrt{\\frac{1}{2 Q_0}} $$For the RO3003 substrate at 8.75 GHz, an estimated quality factor of $Q_0 \\approx 40$ yields a truncation length of $dL \\approx 1.02$ mm. This perturbation splits the fundamental mode into two orthogonal modes with the required 90° phase shift for circular polarization.\nFinal Optimized Parameters #In HFSS, the dimensions derived from the equations were modified to meet the desired specification of the antennas. The final parameters were obtained through a combination of parametric sweeps and optimization runs.\nParameter Value Substrate Rogers RO3003 Relative permittivity 3.0 Loss tangent 0.0013 Substrate thickness 0.508 mm Substrate / ground size 35 × 35 mm Patch width W 9.00 mm Patch length L 9.25 mm Chamfer cut dL 1.8 mm Feed offset y₀ 2.3 mm The final patch after optimization is slightly nonsquare (9.00 × 9.25 mm) to simultaneously satisfy bandwidth and axial ratio requirements.\nSingle Element Simulation Results #S11 — Impedance Bandwidth #Two resonant dips appear at approximately 8.5 GHz and 9.0 GHz, corresponding to the two orthogonal modes introduced by the chamfer. Both remain below -10 dB, yielding a simulated bandwidth of 8.353 – 9.307 GHz (10.8%), comfortably exceeding the 8.5 – 9.0 GHz requirement.\nAxial Ratio #At broadside (θ = 0°), an axial ratio of 2.89 dB was achieved at 8.75 GHz, confirming good circular polarization quality just under the 3 dB requirement.\nPolarization Performance #The RHCP realized gain exceeds the LHCP gain by 15.68 dB at broadside, meeting the ≥ 15 dB isolation requirement. The patch radiates left-hand circular polarization (LHCP).\nGain and Directivity # Parameter Value Maximum realized gain 6.43 dBic Maximum directivity 6.55 dBi Performance vs. Requirements # Parameter Requirement Simulated Met? Center frequency 8.75 GHz 8.83 GHz Yes Bandwidth 8.5 – 9.0 GHz 8.353 – 9.307 GHz Yes Axial ratio \u0026lt; 3 dB 2.89 dB Yes RHCP/LHCP difference ≥ 15 dB 15.68 dB Yes 4-Element Circular Array #Configuration #Four single elements were arranged in a circular array and independently fed with equal amplitude and phase. The array radius was evaluated at three spacings relative to the free-space wavelength at 8.75 GHz (λ ≈ 34.3 mm):\nConfiguration Spacing Radius Config 1 0.5λ 17.15 mm Config 2 0.75λ 25.73 mm Config 3 1.0λ 34.30 mm Mutual Coupling #S-parameter analysis was performed for both single-port excitation (passive) and all-port excitation (active) at each spacing. Across all three configurations, coupling between elements (S21, S31) remained consistently below -20 dB, indicating minimal mutual coupling. With all four ports active, each element maintained its impedance bandwidth within specification.\nRadiation Pattern Calculation #Array patterns were computed using pattern multiplication in MATLAB — the single-element HFSS gain pattern (converted to linear scale) was combined with the analytically derived array factor for a circular array:\nAF(theta, phi) = sum over n of exp(j*k*(x_n*sin(theta)*cos(phi) + y_n*sin(theta)*sin(phi))) MATLAB results were compared against HFSS full-array simulations and showed good agreement across all three spacings.\nArray Observations # The 0.5λ configuration produced the highest total realized gain with wide, clean main beam and low sidelobes The 0.75λ and 1.0λ configurations produced narrower main beams but with significantly larger sidelobes No grating lobes were observed at 0.5λ spacing; sidelobe performance degraded at larger radii as expected Fabrication and Measurement #The single-element antenna was milled from Rogers RO3003 substrate using a Bantam Tools Desktop CNC PCB Mill. A coaxial probe feed was implemented with an SMA connector soldered through the ground plane to the patch.\nThe fabricated antenna was measured using an R140 1-port VNA. The measured S11 showed a clear resonance at approximately 9.3 GHz — higher than the simulated 8.75 GHz.\nDiagnosing the Frequency Shift #A parametric HFSS sweep over εr was conducted to identify the true substrate permittivity. Shifting εr from 3.0 to 2.85 moved the upper resonant dip to 9.3 GHz, matching the measured result. The slight permittivity variation is consistent with typical manufacturing tolerances in Rogers substrates.\nAdditional fabrication factors that likely contributed to the shift:\nCNC milling tolerances on patch edge dimensions SMA connector required physical modification; connector body required clearance from the ground plane to avoid shorting Key Takeaways #On CP design: The chamfer size dL simultaneously controls axial ratio and S11 dip separation — they are not independent parameters. Increasing dL improves axial ratio but spreads the two resonant dips apart, risking a mid-band peak. Finding the right dL is a direct tradeoff between CP quality and impedance bandwidth, which simulation makes much clearer than equations alone.\nOn HFSS optimization: When Optimetrics wouldn\u0026rsquo;t converge, understanding the behavior of gradient-based vs. pattern search algorithms and choosing appropriate parameter starting points was critical. Cost function weighting on axial ratio vs. S11 helped guide the optimizer to a useful solution.\nOn fabrication: Exporting to Gerber for the Bantam mill required careful attention to layer ordering and drill file formatting. Physical connector modification and ground plane trimming were needed post-fabrication before measurement was possible.\nTools Used # Ansys HFSS — full-wave EM simulation, parametric sweeps, Optimetrics optimization MATLAB — array factor calculation, pattern multiplication, data visualization Bantam Tools CNC Mill — PCB fabrication R140 VNA — S11 measurement ","date":"1 May 2026","permalink":"https://scast3.github.io/projects/patch-antenna/","section":"Projects","summary":"test","title":"X-band Patch Antenna and Array Design/Fabrication "},{"content":"Project Summary #This project simulates the effects of receiving a wireless signal that has been altered by carrier offsets and channel noise. I modeled the channel effects as a nonlinear state space system and then implemented an Extended Kalman Filter (EKF) to estimate these offsets and correct them. I decided to apply this to a quadrature shift phase keying (QPSK) modulation scheme since it is widely used in satellite communications which regularly have to correct doppler shifts. I am stil working on the real-time offset correction to turn this into a phase-locked loop (PLL) and potentially implement on a microcontroller. The simulation and algorithms were developed with Matlab and Simulink.\nDownload Full Report\nBackground #In a digital communication system, accurate demodulation requires the receiver to maintain synchronization with the incoming RF carrier. In practice, several physical effects distort the received signal and make this challenging.\nOne of the most prominent effects is carrier frequency offset (CFO) which occurs when the receiver\u0026rsquo;s local oscillator is not perfectly synchronized with the transmitter\u0026rsquo;s. This can occur due to oscillator inaccuracies or Doppler shifts. These frequency offsets accumulate over time and result in a rotating phase in the received baseband signal.\nAdditional distortions include signal amplitude variations due to channel fading and additive noise from receiver electronics.\nTogether, these distortions make accurate symbol detection difficult, and the receiver must estimate these carrier offsets in order to recover the transmitted symbol.\nSystem Model #QPSK Signal #In QPSK, each transmitted symbol sits on one of four constellation points on the unit circle, each separated by 90°:\n$$s_k \\in \\left\\{ \\frac{\\pm 1 \\pm j}{\\sqrt{2}} \\right\\}$$The received baseband signal after passing through the channel is modeled as:\n$$ r_k=a_k s_k e^{j\\theta_k}+v_k $$where the distortions are $\\theta_k$, the carrier phase offset, $a_k$, the signal amplitude, and $v_k \\sim \\mathcal{CN}(0,\\sigma^2)$, complex additive white Gaussian noise (AWGN).\nSeparating into real and imaginary components gives the in-phase (I) and quadrature (Q) components of the received symbol: $$ \\begin{aligned} \\Re(r_k) \u0026= a_k \\big( \\Re(s_k)\\cos\\theta_k - \\Im(s_k)\\sin\\theta_k \\big) + \\Re(v_k), \\\\ \\Im(r_k) \u0026= a_k \\big( \\Re(s_k)\\sin\\theta_k + \\Im(s_k)\\cos\\theta_k \\big) + \\Im(v_k). \\end{aligned} $$State Vector #I chose states based on real world phenomena. A fast-moving satellite will introduce a varying frequency shift due to the doppler effect. In addition, the amplitude will be affected by channel noise. The following four internal states were tracked by the estimator:\nState Symbol Description Carrier phase $\\theta_k$ Instantaneous phase offset Frequency offset $\\omega_k$ CFO from oscillator mismatch Frequency drift $\\alpha_k$ Rate of change of frequency offset Signal amplitude $a_k$ Channel fading amplitude This gives a state vector $x_k$: $$ x_k = \\begin{bmatrix} \\theta_k \\\\ \\omega_k \\\\ \\alpha_k \\\\ a_k \\end{bmatrix}. $$State Equations #The state equations were derived from the integrator relationships between $\\theta_k$ and $\\omega_k$. For this project, the integration is expanded to include frequency drift $\\alpha_k$ which is the discrete-time change in $\\omega_k$.\nThe input $u_k$ is applied to the receiver oscillator which allows adjustment of the frequency offset. This input influences both phase and frequency due to their integrator relationship. In this model, $u_k$ appears in both the phase and frequency equations with a gain of $T$ in the phase equation reflecting the integration of frequency over one symbol period, and a gain of $1$ in the frequency equation as a direct offset correction.\nFurthermore, for model simplicity, the amplitude $a_k$ is modeled as a random walk driven by process noise.\nLastly, for each state, a random noise variable in the vector $n_k \\sim \\mathcal{N}(0,Q)$ is added to finalize the state equation for this system:\n$$ \\begin{aligned} \\theta_{k+1} \u0026= \\theta_k + T\\omega_k + \\frac{T^2}{2}\\alpha_k- T u_k + n_{\\theta,k} \\\\ \\omega_{k+1} \u0026= \\omega_k + T\\alpha_k - u_k + n_{\\omega,k} \\\\ \\alpha_{k+1} \u0026= \\alpha_k + n_{\\alpha,k} \\\\ a_{k+1} \u0026= a_k + n_{a,k} \\end{aligned} $$The full discrete-time state equation in matrix form is:\n$$ x_{k+1} = \\underbrace{ \\begin{bmatrix} 1 \u0026 T \u0026 \\frac{T^2}{2} \u0026 0 \\\\ 0 \u0026 1 \u0026 T \u0026 0 \\\\ 0 \u0026 0 \u0026 1 \u0026 0 \\\\ 0 \u0026 0 \u0026 0 \u0026 1 \\end{bmatrix} }_{\\Phi} x_k + \\underbrace{ \\begin{bmatrix} -T \\\\ -1 \\\\ 0 \\\\ 0 \\end{bmatrix} }_{\\Gamma} u_k + n_k $$or simply:\n$$x_{k+1}=\\Phi x_k + \\Gamma u_k + n_k$$Measurement Equation #The measurement is the received IQ sample, split into in-phase and quadrature components with added measurement noise $v_k \\sim \\mathcal{N}(0,R)$:\n$$ y_k = h(x_k, s_k) + v_k $$$$ h = \\begin{bmatrix} a_k\\big(\\Re(s_k)\\cos\\theta_k - \\Im(s_k)\\sin\\theta_k\\big) \\\\ a_k\\big(\\Re(s_k)\\sin\\theta_k + \\Im(s_k)\\cos\\theta_k\\big) \\end{bmatrix} $$The transmitted symbol $s_k$ is assumed known at the receiver from pilot symbols. The process model is linear; the measurement equation is nonlinear, which justifies the use of the EKF rather over the standard Kalman filter.\nSimulation Parameters # Parameter Value Number of symbols N 500 Symbol period T 1×10⁻⁴ s Symbol rate 10,000 symbols/s Initial phase offset $\\theta_0$ π/3 rad Initial frequency offset $\\omega_0$ 2π×200 rad/s Initial frequency drift $\\alpha_0$ 2π×0.5 rad/s² Initial amplitude $a_0$ 1.0 SNR 10 dB Channel Distortion Simulations #Before applying the EKF, two open-loop scenarios were simulated in Simulink to understand the effect of the carrier offsets. The plant model can be seen below:\nNo Synchronization ($u_k = 0$) #To establish a baseline, the system was first simulated with no synchronization applied, meaning $u_k=0$ for all $k$. In this scenario, no correction is applied to the receiver oscillator. This allows the carrier phase and frequency offset evolve freely according to the model.\nThe received constellation shown in the figure above depicts a ring of samples rather than clustering. This shape occurs because the CFO causes the constellation to rotate continuously, essentially \u0026ldquo;smearing\u0026rdquo; the samples uniformly around the unit circle. This behavior is expected when there is no synchronization since the receiver has no way of accounting for the phase rotation. Furthermore, the radial thickness of the ring reflects the amplitude variation in $a_k$ over the simulation.\nIdeal Impulse Correction ($u_k = \\omega_0 \\cdot \\delta[k]$) #To show the effect of a perfect frequency correction, a unit-impulse input was applied at $k=0$ with magnitude equal to the true frequency offset $\\omega_0$. It is important to note that this scenario is not physically realizable in practice, since it assumes perfect knowledge of the frequency offset at $k=0$. However, it is useful to isolate the effect of the control input.\nThe CFO correction can be seen in the above figure. Instead of the constellation ring as when $u_k=0$, the received samples now cluster around four distinct regions, essentially eliminating the smearing. However, the clusters are still phase shifted from the ideal QPSK constellation points by $\\theta_0 = \\pi/3$. This is a result of the uncompensated initial phase offset which demonstrates the limitation of frequency-only correction. Complete synchronization must estimate and compensate all 4 internal states, not just oscillator frequency. This once again justifies the use of the EKF for state estimation.\nState Estimation with Extended Kalman Filter #As mentioned before, the measurement function $h$ is nonlinear due to the trigonometric dependence of the received IQ samples on the carrier phase $\\theta_k$. The EKF was chosen because it linearizes $h$ about the current state estimate at each time step and applies the standard Kalman filter equations to the linearized model.\nMeasurement Jacobian #Looking back at the state space system, we can see that only the measurement equation requires linearization. The EKF linearizes $h$ by computing the Jacobian matrix $H_k$, defined as the matrix of partial derivatives of $h$ with respect to each state, evaluated at the current predicted state estimate $\\hat{x}_{k}^{(-)}$:\n$$ H_k = \\frac{\\partial h}{\\partial x}\\Bigg|_{\\hat{x}_{k}^{(-)}} $$The Jacobian can be expressed compactly by defining: $$ A_k = \\Re(s_k)\\cos\\hat{\\theta}_k - \\Im(s_k)\\sin\\hat{\\theta}_k $$ $$ B_k = \\Re(s_k)\\sin\\hat{\\theta}_k + \\Im(s_k)\\cos\\hat{\\theta}_k $$giving the linearized measurement matrix:\n$$ H_k = \\frac{\\partial h}{\\partial x}\\Bigg|_{\\hat{x}_{k}^{(-)}}=\\begin{bmatrix} -a_k*B_k \u0026 0 \u0026 0 \u0026 A_k \\\\\\\\ a_k*A_k \u0026 0 \u0026 0 \u0026 B_k \\end{bmatrix} $$Note: because $\\omega_k$ and $\\alpha_k$ are not present in the measurement equation, their corresponding columns are zero. They are observable only indirectly through the accumulation of phase over time.\nEKF Algorithm #The EKF operates recursively at each symbol period $k$, alternating between a measurement update step and a time propagation step.\nInitialization — The filter starts cold with no prior knowledge of phase or frequency:\nAt $k=0$, the state estimate and error covariance are initialized as:\n$$ \\hat{x}_{0} = \\begin{bmatrix} 0 \\\\ 0 \\\\ 0 \\\\ a_\\text{nom} \\end{bmatrix}, $$ $$ P_{0} = \\begin{bmatrix} (\\pi/2)^2 \u0026 0 \u0026 0 \u0026 0 \\\\ 0 \u0026 (2\\pi \\times 500)^2 \u0026 0 \u0026 0 \\\\ 0 \u0026 0 \u0026 (2\\pi \\times 5)^2 \u0026 0 \\\\ 0 \u0026 0 \u0026 0 \u0026 0.25 \\end{bmatrix} $$Step 1 — Measurement Update:\nGiven the predicted state estimate $\\hat{x}_{k}^{(-)}$ and predicted covariance $P_{k}^{(-)}$ from the previous propagation step, the EKF update equations are:\n$$ \\bar{K_k} = P^{(-)}_{k} H_k^T (H_k P^{(-)}_{k} H_k^T + R)^{-1} $$ $$ \\hat{x}_{k}^{(+)} = \\hat{x}_{k}^{(-)} + \\bar{K_k} (y_k - h(\\hat{x}_{k}^{(-)}, s_k)) $$ $$ P_{k}^{(+)} = (I - \\bar{K_k} H_k) P_{k}^{(-)} (I - \\bar{K_k} H_k)^T + \\bar{K_k} R \\bar{K_k}^T $$Step 2 — Time Propagation:\nAfter the measurement update, the state estimate and covariance are propagated forward one symbol period using the linear process model:\n$$ \\hat{x}_{k+1}^{(-)} = \\Phi\\, \\hat{x}_{k}^{(+)} + \\Gamma u_k $$ $$ P_{k+1}^{(-)} = \\Phi\\, P_{k}^{(+)}\\, \\Phi^T + Q $$Because the process model is linear, no Jacobian is required in the prediction step. All linearization error in the EKF is confined to the measurement update.\nMatlab EKF Implementation #The recursive algorithm was implemented in matlab using the system results from the simulink open-loop model. The following snippet shows the EKF loop at each timestep $k$:\nfor k = 1:n_samples %% Step 1: Measurement Update sk = s(k); % known pilot symbol s_re = real(sk); s_im = imag(sk); theta = x_hat(1); a = x_hat(4); % Predicted measurement h h = [a*(s_re*cos(theta) - s_im*sin(theta)); a*(s_re*sin(theta) + s_im*cos(theta))]; % Calculating Jacobian dh_dtheta_I = -a*(s_re*sin(theta) + s_im*cos(theta)); dh_dtheta_Q = a*(s_re*cos(theta) - s_im*sin(theta)); dh_da_I = s_re*cos(theta) - s_im*sin(theta); dh_da_Q = s_re*sin(theta) + s_im*cos(theta); H = [dh_dtheta_I, 0, 0, dh_da_I; dh_dtheta_Q, 0, 0, dh_da_Q]; % Kalman gain K = P * H\u0026#39; / (H * P * H\u0026#39; + R); % State update y_k = y_meas_sim(k,:)\u0026#39;; x_hat = x_hat + K * (y_k - h); % Covariance update IKH = eye(4) - K*H; P = IKH * P * IKH\u0026#39; + K * R * K\u0026#39;; % Store results x_est(k,:) = x_hat\u0026#39;; P_diag(k,:) = diag(P)\u0026#39;; K_log(k,:) = K(:,1)\u0026#39;; %% Step 2: Time Propagation if k \u0026lt; n_samples x_hat = Phi * x_hat + Gamma * u_vec(k); P = Phi * P * Phi\u0026#39; + Q; end end Signal Correction and Symbol Recovery #To isolate the transmitted symbol $s_k$, the received sample is rotated by the negative of the estimated phase offset and normalized by the estimated amplitude:\n$$ \\tilde{r}_k = \\frac{r_k \\cdot e^{-j\\hat{\\theta}_k}}{\\hat{a}_k} $$ Substituting the signal model: $$ \\tilde{r}_k = \\frac{a_k s_k e^{j\\theta_k} e^{-j\\hat{\\theta}_k} + v_k e^{-j\\hat{\\theta}_k}}{\\hat{a}_k} \\approx s_k + \\tilde{v}_k $$ As the EKF converges and $\\hat{\\theta}_k \\to \\theta_k$ and $\\hat{a}_k \\to a_k$, the corrected sample approaches the ideal transmitted symbol plus residual noise only ($\\tilde{v}_k$). A nearest-neighbor decision rule then maps each corrected sample to the closest QPSK constellation point:\n$$ \\hat{s}_k = \\underset{s \\in \\mathcal{S}}{\\arg\\min}\\ |\\tilde{r}_k - s| $$% Rotate received samples back by estimated phase r_received = y_meas_sim(:,1) + 1j*y_meas_sim(:,2); r_corrected = r_received .* exp(-1j*x_est(:,1)) / mean(x_est(:,4)); The decided symbol $\\hat{s}_k$ is taken as the estimate of the transmitted symbol. The effect of the EKF offset estimation and correction can be seen in the figure below by the clustering of symbols around the 4 QPSK constellation points:\nActive Controller Design (Incomplete) #In the current implementation, the oscillator correction input $u_k$ is held at zero throughout the simulation and correction is applied as a post-processing step. In a fully closed-loop receiver, the EKF frequency estimate would be fed back to drive the oscillator in real time:\n$$u_k = \\hat{\\omega}^{(+)}_k$$This would actively reduce the frequency offset at each step, shrinking the residual the filter needs to track and improving both convergence speed and steady-state accuracy. This would essentially be the implementation of a phase locked loop (PLL) which is the next step that I will work on.\nTools Used # MATLAB — EKF implementation, signal correction, constellation plots Simulink — Open-loop state-space plant model View on GitHub →\n","date":"30 April 2026","permalink":"https://scast3.github.io/projects/qpsk-ekf/","section":"Projects","summary":"test","title":"QPSK Carrier Synchronization using Extended Kalman Filter"},{"content":"Overview #This project is the hardware/software co-design of a two-channel oscilloscope on the Xilinx Zynq-7010 SoC. The design captures analog signals from the AD7606 ADC and displays the waveforms in real time over HDMI. The design splits responsibility across two domains on the same chip: programmable logic (PL) which is implemented in VHDL, handles the acquisition and video pipeline which is very timing-dependent, while the ARM Cortex-A9 processing system (PS) runs embedded C firmware for user control through a command line interface.\nThe two domains communicate through a custom AXI4-Lite slave peripheral, giving the ARM processor memory-mapped access to control registers, status flags, and live sample data in the FPGA fabric.\nSystem Architecture #The top-level VHDL entity acquireToHDMI is packaged in an IP named final_oscope and instantiates a datapath acquireToHDMI_datapath handling the ADC interface, waveform buffering, and video rendering. The datapath uses the submodules videoSignalGenerator to generate the HDMI signals and scopeFace to assign the correct RGB values to each coordiate to display the oscilloscope interface. Furthermore, a Moore state machine acquireToHDMI_fsm generates the control word that serves as the control inputs to the logic components inteh datapath.\nThe AXI wrapper final_oscope_slave_lite_v1_0_S00_AXI instantiates this top-level and exposes its ports as memory-mapped registers to the PS. The signals fed from the wrapper are accessed in the file main.c which defines the command-line user interfacing. For function generation, the enhancedPwm IP is used which takes in a duty cycle and outputs the pwm signal.\nThe interaction between the IPs in the PL and the PS through memory-mapped registers can be seen below:\n+---------------------+ AXI4-Lite Bus +----------------------+ | ARM Cortex-A9 (PS) | \u0026lt;-------------------------\u0026gt; | PL (VHDL Fabric) | | | | | | Vitis C firmware | slv_reg0: CH1 data (R) | final_oscope | | - UART CLI | slv_reg1: CH2 data (R) | +-----------------+ | | - TTC0 ISR | slv_reg2: status (R) | | ADC FSM | | | - Trigger control | slv_reg3: control (W) | | Sample timer | | | - Function gen | slv_reg4: trig volt (W) | | Trigger logic | | | | slv_reg5: trig time (W) | | HDMI renderer | | +---------------------+ | +-----------------+ | | | AD7606 ADC --\u0026gt;| 16-bit parallel bus | HDMI output \u0026lt;-| TMDS serializer | +----------------------+ Programmable Logic — VHDL Design #Datapath and Control #The PL follows a standard datapath and control design. The datapath acquireToHdmi_datapath contains all the registers, counters, BRAMs, comparators, and 2\u0026rsquo;s complement pixel converters as structural VHDL instantiations. The control module acquireToHdmi_fsm is a finite state machine that uses the status word sw from the datapath for state transitions. Each state drives a control word cw back to the datapath. The two modules communicate only through these two buses, with no direct logic between them. The datapath additionally manages the TMDS signals required for HDMI display.\nentity acquireToHDMI_datapath is PORT ( clk : in STD_LOGIC; resetn : in STD_LOGIC; cw : in STD_LOGIC_VECTOR(CW_WIDTH -1 downto 0); sw : out STD_LOGIC_VECTOR(DATAPATH_SW_WIDTH - 1 downto 0); an7606data: in STD_LOGIC_VECTOR(15 downto 0); triggerVolt16bitSigned: in SIGNED(15 downto 0); triggerTimePixel: in STD_LOGIC_VECTOR(VIDEO_WIDTH_IN_BITS-1 downto 0); ch1Data16bitSLV, ch2Data16bitSLV: out STD_LOGIC_VECTOR(15 downto 0); ch1enb, ch2enb : in std_logic; tmdsDataP : out STD_LOGIC_VECTOR (2 downto 0); tmdsDataN : out STD_LOGIC_VECTOR (2 downto 0); tmdsClkP : out STD_LOGIC; tmdsClkN : out STD_LOGIC; hdmiOen: out STD_LOGIC; sampleRate_ctrl : in STD_LOGIC_VECTOR(1 downto 0) ); end acquireToHDMI_datapath; Furthermore, a user input to the datapath is the 2-bit sampleRate_ctrl which controls a 4 to 1 multiplexer sampleMux that outputs the sample rate of the ADC acquisition. The design supports four present sampling rates:\nSampling Mode Clock Cycles HIGHEST_RATE 300 HIGH_RATE 600 LOW_RATE 1200 LOWEST_RATE 2400 sampleMux : genericMux4x1 GENERIC MAP(32) PORT MAP( y0 =\u0026gt; HIGHEST_RATE, y1 =\u0026gt; HIGH_RATE, y2 =\u0026gt; LOWEST_RATE, y3 =\u0026gt; LOW_RATE, s =\u0026gt; sampleRate_ctrl, f =\u0026gt; currentRate ); CW and SW Signals #The datapath and control design uses the status and control words to implement the ADC acquisition functionality. The status word is a 10-bit standard logic vector and the control word is a 22-bit standard logic vector. Every resource in the datapath such as counters, registers, BRAM write enables are controlled by a dedicated bit in the cw vector. This makes the state outputs in the FSM completely readable as a lookup table: each state drives a fixed cw binary combination with named bit positions defined in the shared package.\nAll 10 status word bits observed by the FSM:\nBit Description Source in datapath BUSY_SW AD7606 busy signal External ADC pin SHORT_DELAY_DONE_SW Short counter == x10 shortCompare Comparator LONG_DELAY_DONE_SW Long counter == x00FFFF longCompare Comparator FULL_SW BRAM is full: write address == display width cmp_BRAM_full Comparator SAMPLE_SW Sample counter == sampleMux output sampleCompare Comparator TRIG_CH1_SW CH1 rising edge detected Channel 1 trigger comparators TRIG_CH2_SW CH2 rising edge detected Channel 2 trigger comparators STORE_SW Stores ADC samples into BRAM SR latch process FORCED_SW Mode from PS control reg user command (AXI slv_reg3) SINGLE_SW Mode from PS control reg user command (AXI slv_reg3) The FORCED and SINGLE status word bits determine the mode of the oscilloscope and acquisition logic, which are entirely controlled by the user in the PS.\nCondition Mode Description sw(FORCED_SW)==0 Trigger Mode Channel 1 trigger event starts acquisition into BRAM sw(FORCED_SW)==1 Forced Mode User command starts acquisition into BRAM sw(SINGLE_SW)==0 N/A Nothing - User has not yet sent a command sw(SINGLE_SW)==1 Single Acquisition Mode User command starts a single \u0026ldquo;snapshot\u0026rdquo; acquisition Key control word bits driven by the FSM:\nIn each state in the FSM, the module writes a specific 22-bit value to the cw vector which drives the logic components in the datapath.\nBit(s) Function CONVST_CW Assert ADC conversion start CS_CW, RD_CW ADC chip select and read strobe RESET_AD7606_CW ADC hardware reset DATA_STORAGE_CH1_WRITE_CW BRAM write enable for CH1 DATA_STORAGE_CH2_WRITE_CW BRAM write enable for CH2 TRIG_CH1_WRITE_CW Load trigger sample register CH1 TRIG_CH2_WRITE_CW Load trigger sample register CH2 SET_STORE_FLAG_CW / CLEAR_STORE_FLAG_CW Set/clear the SR latch DATA_STORAGE_COUNTER_CW Count/hold/reset BRAM write address SHORT_DELAY_COUNTER_CW / LONG_DELAY_COUNTER_CW Count/hold/reset delay timers SAMPLING_COUNTER_CW Count/hold/reset sample interval timer Finite State Machine Implementation #The FSM has 22 states sequencing the full acquisition pipeline. The major flow is:\nAt each ADC read state, the FSM branches based on STORE_SW: if the SR latch is set (BRAM fill is active), it routes the sample to BRAM (WRITE_CH1_BRAM); otherwise it routes it only to the trigger comparator registers (WRITE_CH1_TRIG). This ensures samples are compared against the threshold continuously but only written to BRAM once a trigger has been detected.\nIn trigger mode: the FSM loops through BEGIN_CONVST continuously, writing samples only to the trigger registers, until TRIG_CH1_SW fires. Then the SET_STORE_FLAG enables BRAM writes and the next VIDEO_WIDTH samples fill the display buffer.\nIn forced mode: the FSM parks in WAIT_FORCED and only proceeds on a SINGLE_SW pulse from the PS, immediately setting the store flag and capturing one frame.\nADC Interface (AD7606) #The ALINX daughter board AN706, contains an Analog Devices AD7606 8-channel 16-bit ADC which was used to digitize the analog input. The AD7606 uses a successive approximation register (SAR) approach. The converter accepts analog input voltages in the range of -5 V to +5 V and produces a signed 16-bit two\u0026rsquo;s-complement output value; it also supports sampling rates up to 200 kS/s and presents the conversion result through a parallel digital interface.\nThe FSM drives the external ADC signals CONVST, CS, RD, and RESET in the correct sequence, asserting conversion start, waiting for the BUSY flag to deassert (states BUSY_0 -\u0026gt; BUSY_1), then clocking out the 16-bit result. Two short-delay counters in the datapath provide the required ADC setup and hold timing. Sampling rate is controlled by a 4-to-1 mux (sampleMux) that selects between four preset counter targets based on the 2-bit sampleRate_select from the PS.\nTrigger Logic #A trigger occurs when the samples cross a certain threshold of triggerVolt16bitSigned which is set by the user in the PS (with a default of 0V). To ensure the trigger is on rising edge, the previous and current sample are tracked. For each channel, two chained register instances capture consecutive ADC samples (sample 1 and sample 2), and two signed comparator instances compare each against the triggerVolt16bitSigned vector. The ch1_sample1_compare comparator checks checks sample1 \u0026gt; threshold (rising condition) and the ch2_sample1_compare comparator checks sample2 \u0026lt; threshold (pre-crossing condition):\n-- ch1 trigger logic ch1_trigger_sample1_signed \u0026lt;= signed(ch1_trigger_sample1); ch1_sample1_compare : genericCompare_Signed GENERIC MAP(16) PORT MAP(x =\u0026gt; ch1_trigger_sample1_signed, y =\u0026gt; triggerVolt16bitSigned, g =\u0026gt; ch1_trigger_sample1_cond, l =\u0026gt; open, e =\u0026gt; open ); ch1_trigger_sample2_signed \u0026lt;= signed(ch1_trigger_sample2); ch1_sample2_compare : genericCompare_Signed GENERIC MAP(16) PORT MAP(x =\u0026gt; ch1_trigger_sample2_signed, y =\u0026gt; triggerVolt16bitSigned, g =\u0026gt; open, l =\u0026gt; ch1_trigger_sample2_cond, e =\u0026gt; open ); sw(TRIG_CH1_SW_BIT_INDEX) \u0026lt;= ch1_trigger_sample1_cond and ch1_trigger_sample2_cond; The trigger occurs when both comparator conditions are true, meaning the signal crosses the threshold on a rising edge. This logic prevents false triggers on a flat signal sitting above the threshold.\nHDMI Video Output and Waveform Rendering #The datapath instantiates a clk_wiz_0 PLL to derive the pixel clock (videoClk) and a 5x clock (videoClk5x) for TMDS serialization from the system clock. In addition, video control was implemented to convert VGA to HDMI format. The control logic was responsible for displaying the grid, two trigger markers, and the two channel waveforms. The submodule videoSignalGenerator.vhdl produces standard HS, VS, and DE timing signals along with pixel coordinates (pixelHorz, pixelVert).\nThe submodule scopeFace.vhdl determines the appropriate RGB values at each pixel location. These RGB values are dependent on the type of item being drawn.\nIn the scopeToHdmi_package.vhdl, the RGB values for each region of the display are declared as constants:\n-- Display border - white constant BORDER_R : STD_LOGIC_VECTOR(7 downto 0) := X\u0026#34;FF\u0026#34;; constant BORDER_G : STD_LOGIC_VECTOR(7 downto 0) := X\u0026#34;FF\u0026#34;; constant BORDER_B : STD_LOGIC_VECTOR(7 downto 0) := X\u0026#34;FF\u0026#34;; -- Grid, tickmarks, and major axes - gray constant GRID_R : STD_LOGIC_VECTOR(7 downto 0) := X\u0026#34;40\u0026#34;; constant GRID_G : STD_LOGIC_VECTOR(7 downto 0) := X\u0026#34;40\u0026#34;; constant GRID_B : STD_LOGIC_VECTOR(7 downto 0) := X\u0026#34;40\u0026#34;; -- Channel 1 - yellow constant CH1_R : STD_LOGIC_VECTOR(7 downto 0) := X\u0026#34;FD\u0026#34;; constant CH1_G : STD_LOGIC_VECTOR(7 downto 0) := X\u0026#34;FF\u0026#34;; constant CH1_B : STD_LOGIC_VECTOR(7 downto 0) := X\u0026#34;00\u0026#34;; -- Channel 2 - green constant CH2_R : STD_LOGIC_VECTOR(7 downto 0) := X\u0026#34;00\u0026#34;; constant CH2_G : STD_LOGIC_VECTOR(7 downto 0) := X\u0026#34;FF\u0026#34;; constant CH2_B : STD_LOGIC_VECTOR(7 downto 0) := X\u0026#34;1C\u0026#34;; -- Trigger Arrows - cyan constant TRIGGER_R : STD_LOGIC_VECTOR(7 downto 0) := X\u0026#34;00\u0026#34;; constant TRIGGER_G : STD_LOGIC_VECTOR(7 downto 0) := X\u0026#34;FF\u0026#34;; constant TRIGGER_B : STD_LOGIC_VECTOR(7 downto 0) := X\u0026#34;FF\u0026#34;; Each channel\u0026rsquo;s BRAM is dual-port: port A is clocked on the system clock and written by the FSM during acquisition; port B is clocked on the pixel clock and read during display. The read address is pixelHorz - L_EDGE, mapping each horizontal pixel directly to a stored sample. A toPixelValue converter scales the 16-bit signed ADC value to a vertical pixel coordinate, and a genericCompare checks whether the current pixelVert matches — driving the ch1 / ch2 signals into the scopeFace renderer which composites the waveform, grid, and trigger markers into RGB pixel values for the hdmi_tx_0 serializer.\nThe logic of the datapath which includes the hdmi display with BRAM waveform rendering can be seen in the block diagram below:\nThe final result is an IP that displays channel data from the ADC to a standard oscilloscope HDMI display using datapath and control design logic. The following image shows the display when both channels are connected to an external function generator:\nHowever, to complete the design, a seperate IP had to be created for custom function generation.\nPWM Function Generation IP #A seperate enhancedPwm IP was created to add waveform generation functionality using pulse width modulation (PWM). The module enhancedPwm.vhdl contains the PL functionality for generating a pwm signal for function generation.\nentity enhancedPwm is Port ( clk : in STD_LOGIC; resetn : in STD_LOGIC; dutyCycle : in STD_LOGIC_VECTOR (8 downto 0); enb : in STD_LOGIC; pwmSignal : out STD_LOGIC; pwmCount : out STD_LOGIC_VECTOR (7 downto 0); rollOver : out STD_LOGIC); end enhancedPwm; The module contains an 8-bit free-running counter pwmCount that repeatedly counts from 0 to 255. A comparator continuously compares the current counter value to the 9-bit duty_cycle input. The PWM output goes high when the duty cycle value is greater than the counter value, producing a pulse train whose duty cycle is proportional to the input sample value.\nThe enhancedPwm IP is integrated with the final_oscope IP by using the waveform samples stored in BRAM as the duty cycle input. The waveform samples are read sequentially from memory and supplied to the PWM module. Each stored sample determines the duty cycle for one PWM period, generating a wave with sharp jumps between samples. Because of this, a low pass filter is applied and the PWM signal is reconstructed into a continuous analog voltage waveform.\nThis allows the captured ADC data to be stored in memory, displayed on the HDMI oscilloscope interface, and sent through the PWM output using the same sample buffer. The PL implements both waveform acquisition and waveform generation using a common BRAM-based data path.\nAXI4-Lite Slave Wrapper #The AXI wrapper final_oscope_slave_lite_v1_0_S00_AXI.vhdl implements a custom AXI4-Lite slave with 10 32-bit registers with designated read/write functionality. In this design, only the first 5 registers are used; trigger time is mapped to slv_reg5 but never used in the PS. The register map exposes the oscilloscope IP to the ARM:\nRegister Direction Contents slv_reg0 Read CH1 sample data (16-bit) slv_reg1 Read CH2 sample data (16-bit) slv_reg2 Read Status flags slv_reg3 Write Control register slv_reg4 Write Trigger voltage (signed 16-bit) slv_reg5 Write Trigger time (signed 16-bit) - unused The bit mapping for the control and status registers is implemented during the oscilloscope IP instantiation:\nsignal ch1_data_int : std_logic_vector(C_S_AXI_DATA_WIDTH-1 downto 0); -- read reg 0 signal ch2_data_int : std_logic_vector(C_S_AXI_DATA_WIDTH-1 downto 0); -- read reg 1 signal status_reg_int : std_logic_vector(C_S_AXI_DATA_WIDTH-1 downto 0); -- read reg 2 ... oscope_inst : acquireToHdmi PORT MAP( clk =\u0026gt; S_AXI_ACLK, resetn =\u0026gt; S_AXI_ARESETN, flag_clear =\u0026gt; slv_reg3(7), flag_q =\u0026gt; status_reg_int(4), single_mode =\u0026gt; slv_reg3(0), forced_mode =\u0026gt; slv_reg3(1), ch1enb =\u0026gt; slv_reg3(2), ch2enb =\u0026gt; slv_reg3(3), sampleRate_select =\u0026gt; slv_reg3(5 downto 4), triggerCh1 =\u0026gt; status_reg_int(0), triggerCh2 =\u0026gt; status_reg_int(1), conversionPlusReadoutTime =\u0026gt; status_reg_int(2), sampleTimerRollover =\u0026gt; status_reg_int(3), triggerVolt16bitSigned =\u0026gt; signed(slv_reg4(15 downto 0)), triggerTime =\u0026gt; slv_reg5(VIDEO_WIDTH_IN_BITS-1 downto 0), ch1Data16bitSLV =\u0026gt; ch1_data_int(15 downto 0), ch2Data16bitSLV =\u0026gt; ch2_data_int(15 downto 0), -- ADC and TMDS signal assignments not shown ... ); Control register (slv_reg3) bit map:\nBit Function 0 single_mode — pulse to acquire one frame 1 forced_mode — run continuously without trigger 2 ch1enb — enable channel 1 3 ch2enb — enable channel 2 5:4 sampleRate_select — 2-bit sample rate 6 Reset pulse (not used) 7 Flag clear — acknowledge sample-ready flag Status register (slv_reg2) bit map:\nBit Function 0 triggerCh1 — CH1 threshold crossed 1 triggerCh2 — CH2 threshold crossed 2 conversionPlusReadoutTime — ADC busy window 3 sampleTimerRollover — sample period elapsed 4 flag_q — new sample ready flag Additionally, a second custom AXI4-Lite peripheral was used to implement the software-controlled function generator with enhancedPwm. In this wrapper, the duty cycle is mapped to the lower 9 bits of the write register slv_reg0 and the pwm count is mapped to the lower 8 bits of the read register slv_reg1:\nenhancedPwm_inst : enhancedPWM PORT MAP( clk =\u0026gt; S_AXI_ACLK, resetn =\u0026gt; S_AXI_ARESETN, enb =\u0026gt; enb_ext, dutyCycle =\u0026gt; slv_reg0(8 downto 0), pwmCount =\u0026gt; pwmCount_int(7 downto 0), rollOver =\u0026gt; rollOver_ext, pwmSignal =\u0026gt; pwmSignal_ext ); The final design was implemented in Vivado as a Zynq-based system integrating the Processing System with custom AXI4-Lite IP blocks final_oscope and enhancedPwm, then synthesized into a single FPGA bitstream that was then accessed by the Vitis application for embedded firmware development. The block diagram of the Vivado design used to generate the bitstream can be seen below:\nProcessing System — ARM Cortex-A9 (Embedded C) #After completing the memory mapping, the firmware was designed under Xilinx Vitis (bare-metal, no OS) which provides a UART command-line interface for real-time oscilloscope control. The C code accesses the read and write registers passed through by the AXI wrapper.\nUART Command Interface #The user interacts and controls the system through a UART based command-line interface. This was implemented in the main loop which blocks on XUartPs_RecvByte() and dispatches on a single character:\nKey Action t Toggle trigger / forced acquisition mode n Single-shot acquire (pulse single_mode bit high then low) + / - Increment / decrement trigger voltage by 1000 LSB v Reset trigger voltage to 0 a / b Toggle CH1 / CH2 enable s Toggle function generator on/off w Select sine or sinc waveform P Change function frequency d Set PWM duty cycle manually u Read and print 64 sequential samples from CH1 r Universal reset (not implemented) ? Print help menu Many of the functions read and write to/from the memory mapped registers. This communication between PS and PL uses the generated FINAL_OSCOPE_mReadReg, FINAL_OSCOPE_mWriteReg, ENHANCEDPWM_AXI_mReadReg, and ENHANCEDPWM_AXI_mWriteReg macros.\nFunction Generation using Direct Digital Synthesis (DDS) #Case w allows the user to determine whether to generate a sine or sinc wave and case P allows the user to define the frequency. This was done with a software based DDS engine which was implemented to generate programmable waveforms for the oscilloscope and the Triple Timer Counter (TTC0) which generated interrupts at 10 kHz.\nWithin the interrupt service routine (ISR), a 16-bit phaseAccumulator is advanced by a configurable phaseIncrement. The upper bits of the accumulator are used to index a 64-point lookup table containing either sine or sinc waveform samples. The resulting sample is then written directly into the FPGA fabric via the memory-mapped register slv_reg0 of the enhancedPwm IP using ENHANCEDPWM_AXI_mWriteReg, where it is consumed as the PWM duty cycle input.\nstatic void Ttc0IsrHander(void *CallBackRef, u32 StatusEvent) { static u16 phaseAccumulator = 0; u16 lutIndex = 0; u8 dutyCycleValue = 128; // Do ISR stuff here if (generateWave == TRUE) { phaseAccumulator += phaseIncrement; lutIndex = (phaseAccumulator \u0026gt;\u0026gt; 10); if (currentWaveform == WAVE_SINE) { dutyCycleValue = sinLut[lutIndex]; } else { dutyCycleValue = sincLut[lutIndex]; } ENHANCEDPWM_AXI_mWriteReg(XPAR_ENHANCEDPWM_AXI_0_BASEADDR , DUTY_CYCLE_OFFSET, dutyCycleValue); } } By varying the phase increment, the frequency of the waveform could be adjusted independently of the interrupt rate which allows precise digital frequency synthesis without modifying timer configuration.\nUsing a linear regression where I experimented with different phase increments and measured the output function frequency with a Keysight oscilloscope, it was determined that the association between frequency and phase increment was phaseIncrement = 6.5516 * frequency + 0.0062. This equation gives the user the option to define the desired function frequency via the UART interface.\nChanging Sampling Rates #The m command allows the user to choose one of the four sampling rates. This control is implemented with a simple write to bits 4 and 5 of slv_reg3:\nu32 reg3 = FINAL_OSCOPE_mReadReg(XPAR_FINAL_OSCOPE_0_BASEADDR, FINAL_OSCOPE_S00_AXI_SLV_REG3_OFFSET); // clear bits 5:4 reg3 \u0026amp;= ~(0x3 \u0026lt;\u0026lt; 4); switch(c) { case \u0026#39;0\u0026#39;: reg3 |= (0 \u0026lt;\u0026lt; 4); printf(\u0026#34;Sample rate = 0\\r\\n\u0026#34;); break; // other cases not shown } FINAL_OSCOPE_mWriteReg(XPAR_FINAL_OSCOPE_0_BASEADDR, FINAL_OSCOPE_S00_AXI_SLV_REG3_OFFSET, reg3); Toggling Oscilloscope Modes #The user can change the oscilloscope mode between trigger and forced using the t command. The mode is controlled by bit 1 in the memory mapped slv_reg3. This bit selects between trigger and forced mode. A FORCED_MASK (1\u0026lt;\u0026lt;1) is used to isolate the corresponding control bit without affecting the other bits in the control register.\nu32 slv3_read = FINAL_OSCOPE_mReadReg(XPAR_FINAL_OSCOPE_0_BASEADDR, FINAL_OSCOPE_S00_AXI_SLV_REG3_OFFSET); u32 updated_reg = slv3_read ^ FORCED_MASK; int new_forced_bit = (updated_reg \u0026gt;\u0026gt; 1) \u0026amp; 1; if (new_forced_bit == 1) { printf(\u0026#34;FORCED MODE - wait for button press\\r\\n\u0026#34;); } else { printf(\u0026#34;TRIGGER MODE\\r\\n\u0026#34;); } FINAL_OSCOPE_mWriteReg(XPAR_FINAL_OSCOPE_0_BASEADDR, FINAL_OSCOPE_S00_AXI_SLV_REG3_OFFSET, updated_reg ); Toggling is performed via an XOR operation so that repeated user inputs flip the mode deterministically. The updated register value is written back over AXI4-Lite, which immediately updates the acquisition logic in the FPGA fabric in real time.\nWhen in forced mode, the user can do a single shot acquisition with the n command. This is controlled with bit 0 of slv_reg3. The SINGLE_MASK (1 \u0026lt;\u0026lt; 0) is used to target this bit:\nu32 slv3_read_single = FINAL_OSCOPE_mReadReg(XPAR_FINAL_OSCOPE_0_BASEADDR, FINAL_OSCOPE_S00_AXI_SLV_REG3_OFFSET); u32 reg_high = slv3_read_single | SINGLE_MASK; // set high regardless FINAL_OSCOPE_mWriteReg(XPAR_FINAL_OSCOPE_0_BASEADDR, FINAL_OSCOPE_S00_AXI_SLV_REG3_OFFSET, reg_high); u32 reg_low = reg_high \u0026amp; (~SINGLE_MASK); // clear bit 0 FINAL_OSCOPE_mWriteReg(XPAR_FINAL_OSCOPE_0_BASEADDR, FINAL_OSCOPE_S00_AXI_SLV_REG3_OFFSET, reg_low); The bit is asserted as a brief pulse rather than a latched state. This generates a single-cycle trigger event in the FPGA fabric, doing a single acquisition frame before automatically resetting.\nToggling Channel Enables #The user can enable/disable channel 1 with the a command and channel 2 with the b command. Channel 1 and 2 enable control corresponds to bits 2 and 3 of slv_reg3 respectively. A CH1_TOGGLE_MASK (1 \u0026lt;\u0026lt; 2) is used to isolate this bit while preserving all other configuration fields in the register.\nu32 slv3_read_ch1 = FINAL_OSCOPE_mReadReg(XPAR_FINAL_OSCOPE_0_BASEADDR, FINAL_OSCOPE_S00_AXI_SLV_REG3_OFFSET); u32 updated_ch1 = slv3_read_ch1 ^ CH1_TOGGLE_MASK; int new_bit_value_ch1 = (updated_ch1 \u0026gt;\u0026gt; 2) \u0026amp; 1; if (new_bit_value_ch1 == 1) { printf(\u0026#34;Channel 1 on\\r\\n\u0026#34;); } else { printf(\u0026#34;Channel 1 off\\r\\n\u0026#34;); } FINAL_OSCOPE_mWriteReg(XPAR_FINAL_OSCOPE_0_BASEADDR, FINAL_OSCOPE_S00_AXI_SLV_REG3_OFFSET, updated_ch1 ); Similar to the oscilloscope mode control, the XOR operation toggles the enable state deterministically on each user command.\nModifying Trigger Voltage #The trigger voltage is stored in the lower 16 bits of slv_reg4 as a signed value. The user can increase/decrease this value with the commands + and - respectively. When the user adjusts the trigger level, the firmware reads the full register, extracts bits [15:0] as an int16_t, applies an increment or decrement (+/-1000), and then writes the updated value back through AXI4-Lite. This ensures only the trigger threshold is modified without affecting unrelated control fields. A v command to reset simply overwrites the lower 16 bits with zero while leaving the upper register contents unchanged.\nExample of the update mechanism:\n// Read signed 16-bit trigger voltage u32 full = FINAL_OSCOPE_mReadReg(XPAR_FINAL_OSCOPE_0_BASEADDR, FINAL_OSCOPE_S00_AXI_SLV_REG4_OFFSET); int16_t voltage = (int16_t)(full \u0026amp; 0xFFFF); voltage += 1000; // or -1000 FINAL_OSCOPE_mWriteReg(XPAR_FINAL_OSCOPE_0_BASEADDR, FINAL_OSCOPE_S00_AXI_SLV_REG4_OFFSET, (full \u0026amp; 0xFFFF0000) | ((u32)voltage \u0026amp; 0xFFFF)); Spooling Samples #The u command allows the user to read the last 64 waveform samples from the FPGA. The routine first asserts the single mode control bit to enable a single shot acquisition. It then enters a loop to retrieve 64 samples. To ensure that the sample being read by the PS is displayed before getting overwritten by the hardware, a handshake protocol was designed using a flag register. The status bit FLAG_Q_BIT was mapped to bit 4 on slv_reg2 and the FLAG_CLEAR_BIT was mapped to bit 7 on slv_reg3. The appropriate bit masks FLAG_Q_MASK (1 \u0026lt;\u0026lt; FLAG_Q_BIT) and FLAG_CLEAR_MASK (1 \u0026lt;\u0026lt; FLAG_CLEAR_BIT) were used.\nFor each sample, the ARM processor constantly polled slv_reg2 until the FLAG_Q_BIT went high:\ndo { slv2_flag_read = FINAL_OSCOPE_mReadReg(XPAR_FINAL_OSCOPE_0_BASEADDR, FINAL_OSCOPE_S00_AXI_SLV_REG2_OFFSET); } while ((slv2_flag_read \u0026amp; FLAG_Q_MASK) == 0); // Loop until the q bit is 1 Once the ready flag was detected, the contents of slv_reg0 which corresponds to the channel 1 data was read and displayed:\nu32 ch1data_32bit = FINAL_OSCOPE_mReadReg(XPAR_FINAL_OSCOPE_0_BASEADDR, FINAL_OSCOPE_S00_AXI_SLV_REG0_OFFSET); printf(\u0026#34;ch1[%d]: %lu\\r\\n\u0026#34;, i, (unsigned long)ch1data_32bit); To acknowledge that the sample was recieved, the software generated a pulse on the FLAG_CLEAR_BIT to clear the register and notify to the FPGA that the next sample was ready to be loaded:\n// clear flag - set to high then to low u32 reg3_set_clear = FINAL_OSCOPE_mReadReg(XPAR_FINAL_OSCOPE_0_BASEADDR, FINAL_OSCOPE_S00_AXI_SLV_REG3_OFFSET); FINAL_OSCOPE_mWriteReg(XPAR_FINAL_OSCOPE_0_BASEADDR, FINAL_OSCOPE_S00_AXI_SLV_REG3_OFFSET, reg3_set_clear | FLAG_CLEAR_MASK); u32 reg3_clear_done = FINAL_OSCOPE_mReadReg(XPAR_FINAL_OSCOPE_0_BASEADDR, FINAL_OSCOPE_S00_AXI_SLV_REG3_OFFSET); FINAL_OSCOPE_mWriteReg(XPAR_FINAL_OSCOPE_0_BASEADDR, FINAL_OSCOPE_S00_AXI_SLV_REG3_OFFSET, reg3_clear_done \u0026amp; (~FLAG_CLEAR_MASK)); Key Design Decisions #Splitting acquisition and rendering into PL. Doing the ADC sequencing and HDMI pixel rendering in VHDL keeps all hard real-time operations in the fabric and away from the ARM. The PS only needs to write configuration registers and poll status — it never has to meet a pixel clock deadline.\nAXI flag handshake for sample-ready. Rather than polling the ADC busy signal directly from software, the PL sets a flag_q bit in the status register when a new sample is ready, and the PS acknowledges it by pulsing flag_clear. This decouples the ADC conversion timing from the software polling rate and avoids missed samples.\nSigned trigger voltage over AXI. The AD7606 outputs signed 16-bit values, so the trigger threshold needs to be signed too. Passing it through a 32-bit AXI register required explicit masking to avoid sign extension corrupting the upper half of the register — a subtle bug that showed up during integration testing.\nPhase accumulator for waveform generation. Instead of computing sine values in the ISR (too slow for bare-metal at 10 kHz), the ISR uses a 16-bit phase accumulator and a pre-computed 64-entry LUT. The upper 6 bits of the accumulator index into the table, giving smooth frequency control by just changing the increment value.\nTools Used # Xilinx Vivado — VHDL synthesis, implementation, AXI IP packaging Xilinx Vitis — ARM Cortex-A9 bare-metal C firmware ModelSim — VHDL functional simulation Zynq-7010 SoC (Digilent board) — target hardware View on GitHub →\n","date":"10 December 2025","permalink":"https://scast3.github.io/projects/fpga-oscilloscope/","section":"Projects","summary":"Designed an oscilloscope on an fpga in vhdl","title":"FPGA Signal Acquisition on Zynq SoC"},{"content":"Overview #Along with with W. Schageman, developed an intrusion detection system to detect and triangulate the source of WiFi jamming via deauthentication attacks, common in network disruptions. ESP32(s) scan for deauth/dissoc packets in promiscuous mode, relaying alerts to a Raspberry Pi server for logging and analysis. Use Flipper Zero (with WiFi module/ESP32 marauder) to simulate deauthentication attacks in a lab environment. RSSI localization done via least squares multilateration.\nDownload Full Report\nComponents #The system is divided into a distributed data acquisition network and a centralized processing core.\nAttacker Simulation Node: A Flipper Zero equipped with an ESP32 WiFi Development Board running the ESP32 WiFi Marauder firmware. This node simulates real world network disruptions by generating thousands of spoofed 802.11 deauthentication packets per second. Distributed Sensor Network: Three separate ESP32 sensors powered by portable battery packs. These nodes scan the RF spectrum in promiscuous mode to intercept raw 802.11 frames and harvest critical RSSI data. ESP32 Gateway: A localized receiver hub that receives incoming ESP-NOW packets from the sensor nodes and forwards them to the primary compute module over a hardware UART serial interface. Central Compute Module (Raspberry Pi 3B+): The core server responsible for asynchronous data ingestion, timestamp synchronization, in-memory storage, and spatial localization calculations. DISCLAIMER: All testing was done on my home WiFi network as testing in any other public or private space without permission is prohibited and illegal.\nSensor Firmware #The firmware for the ESP32s is implemented in C utilizing the ESP-IDF\nPromiscuous Mode Interception: Sensors bypass standard MAC filtering to capture raw management frames across specified WiFi channels. Sliding Window Detection: Rather than flooding the network with individual alerts, sensors use a temporal sliding window to detect spikes in deauthentication/disassociation frame frequencies. Event Struct Serialization: When an attack threshold is crossed, the sensor constructs an \u0026lsquo;Event\u0026rsquo; data structure. This aggregated payload is transmitted over the ESP-NOW protocol directly to the gateway, optimizing bandwidth and power consumption. Gateway Stream Ingestion: The ESP32 Gateway receives the ESP-NOW payloads, casts the raw bytes back into the standardized \u0026lsquo;Event\u0026rsquo; data type, appends a timestamp, and pushes the data into the hardware UART stream. Raspberry Pi Processing Pipeline #The server software on the Pi is written in C++ and has a multithreaded architecture for guarantee real-time processing without blocking high-frequency UART data ingestion.\nThe system splits execution across 3 threads:\nThe producer thread reads the raw incoming byte stream from the UART interface, casts them to \u0026lsquo;Event\u0026rsquo;, and pushes them into a thread-safe shared queue.\nThe consumer thread pops items off the shared queue, bucket-sorts the events based on their timestamps within an established window, and performs efficient batch insertions into an in-memory DuckDB database.\nThe main thread queries the structured data out of DuckDB to do the localization matrix math and trigger active alerting mechanisms.\nThreat Localization #To pinpoint the spatial location of the attacker, the main thread extracts synchronized RSSI signatures registered by the three discrete ESP32 sensor coordinates during the same attack window.\nSignal Propagation Modeling: RSSI values are converted into relative distance estimates using the Log-Distance Path Loss model. Least Squares Multilateration: The system treats the estimated distances as intersecting circles centered at each known sensor coordinate. It formulates an overdetermined system of non-linear geometric equations and resolves the optimal (x,y) coordinate of the attacker using a least-squares optimization matrix. Results # Conclusion #This project successfully demonstrated that RSSI-based localization using ESP32 receivers can achieve sub-meter accuracy when both sensor geometry and path-loss parameters are properly calibrated. Our experiments showed that geometric layout plays a critical role in determining localization stability, with the equilateral configuration consistently outperforming right-triangle and isosceles arrangements. Additionally, we found that a path-loss exponent of $n = 4$ provided significantly more accurate distance estimates than $n = 3$, highlighting the importance of environment-specific calibration. While RSSI measurements remain inherently noisy, the use of least-squares trilateration combined with stable sensor placement allowed the system to achieve localization errors as low as $0.09$~m. These results indicate that low-cost receivers, when configured correctly, can provide reliable spatial awareness for indoor tracking and intrusion detection applications.\nView on GitHub →\n","date":"10 November 2025","permalink":"https://scast3.github.io/projects/wifi-attack/","section":"Projects","summary":"\u003ch2 id=\"overview\" class=\"relative group\"\u003eOverview \u003cspan class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100\"\u003e\u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\" style=\"text-decoration-line: none !important;\" href=\"#overview\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\u003c/span\u003e\u003c/h2\u003e\u003cp\u003eAlong with with \u003ca href=\"https://github.com/wallyschag\" target=\"_blank\" rel=\"noreferrer\"\u003eW. Schageman\u003c/a\u003e, developed an intrusion detection system to detect and triangulate the source of WiFi jamming via deauthentication attacks, common in network disruptions. ESP32(s) scan for deauth/dissoc packets in promiscuous mode, relaying alerts to a Raspberry Pi server for logging and analysis. Use Flipper Zero (with WiFi module/ESP32 marauder) to simulate deauthentication attacks in a lab environment. RSSI localization done via least squares multilateration.\u003c/p\u003e","title":"Wifi Deauthentication Attack Detection/Localization"},{"content":"","date":null,"permalink":"https://scast3.github.io/tags/c++/","section":"Tags","summary":"","title":"C++"},{"content":"","date":null,"permalink":"https://scast3.github.io/tags/matlab/","section":"Tags","summary":"","title":"Matlab"},{"content":"","date":null,"permalink":"https://scast3.github.io/","section":"Portfolio Page 1","summary":"","title":"Portfolio Page 1"},{"content":"","date":null,"permalink":"https://scast3.github.io/tags/","section":"Tags","summary":"","title":"Tags"},{"content":"","date":null,"permalink":"https://scast3.github.io/tags/vhdl/","section":"Tags","summary":"","title":"VHDL"},{"content":"","date":null,"permalink":"https://scast3.github.io/projects/","section":"Projects","summary":"","title":"Projects"},{"content":"","date":null,"permalink":"https://scast3.github.io/tags/antenna-theory/","section":"Tags","summary":"","title":"Antenna Theory"},{"content":"","date":null,"permalink":"https://scast3.github.io/tags/hfss/","section":"Tags","summary":"","title":"HFSS"},{"content":"","date":null,"permalink":"https://scast3.github.io/tags/rf-design/","section":"Tags","summary":"","title":"RF Design"},{"content":"","date":null,"permalink":"https://scast3.github.io/tags/vna/","section":"Tags","summary":"","title":"VNA"},{"content":"","date":null,"permalink":"https://scast3.github.io/tags/communications/","section":"Tags","summary":"","title":"Communications"},{"content":"","date":null,"permalink":"https://scast3.github.io/tags/dsp/","section":"Tags","summary":"","title":"DSP"},{"content":"","date":null,"permalink":"https://scast3.github.io/tags/kalman-filter/","section":"Tags","summary":"","title":"Kalman Filter"},{"content":"","date":null,"permalink":"https://scast3.github.io/tags/simulink/","section":"Tags","summary":"","title":"Simulink"},{"content":"","date":null,"permalink":"https://scast3.github.io/tags/fpga/","section":"Tags","summary":"","title":"FPGA"},{"content":"","date":null,"permalink":"https://scast3.github.io/tags/verification/","section":"Tags","summary":"","title":"Verification"},{"content":"","date":null,"permalink":"https://scast3.github.io/tags/xilinx/","section":"Tags","summary":"","title":"Xilinx"},{"content":"","date":null,"permalink":"https://scast3.github.io/tags/esp32/","section":"Tags","summary":"","title":"ESP32"},{"content":"","date":null,"permalink":"https://scast3.github.io/tags/raspberry-pi/","section":"Tags","summary":"","title":"Raspberry Pi"},{"content":"","date":null,"permalink":"https://scast3.github.io/tags/wifi/","section":"Tags","summary":"","title":"Wifi"},{"content":" Hello, I am Santiago!\nI\u0026rsquo;m currently an Electrical Engineer at JHU Applied Physics Lab working in the area of Communication Systems. My professional and academic work has been in the wireless PHY layer from receiver algorithm design to real-time DSP pipeline implementation in HDL and system-level software. Throughout my career, I have been passionate about tackling low latency and resource-constrained design challenges.\nI have a master\u0026rsquo;s degree in Electrical Engineering from Colorado School of Mines and dual bachelor\u0026rsquo;s degrees in Computer Science and Mechanical Engineering.\nThe projects in this portfolio reflect my physical layer interests: RTL design, bare-metal firmware, receiver algorithm modeling, and even some RF frontend. Building these projects helped me understand how wireless systems work from antenna to bits.\nTechnical Skills #Digital Signal Processing # Extended Kalman Filter for nonlinear state estimation: carrier phase, frequency offset, drift, and amplitude tracking in QPSK receiver Direct Digital Synthesis using phase accumulator + LUT architecture; ADC interface design (16-bit SAR, ±5 V, 200 kS/s) Real-time digital filtering and spectral analysis on live IQ streams from an SDR receiver Digital filter design (FIR/IIR), state-space modeling in discrete time, and signal simulation in MATLAB and Simulink Real-Time Software Development # Designed real-time UDP IQ packet processing from an SDR, with FIFO buffering to decouple packet arrival from downstream digital filtering Thread synchronization using mutexes in C++ to prevent race conditions between producer and consumer threads SDR radio control via API: Sent acquisition parameters like sampling rate, center frequency, gain, etc via TCP Embedded Systems, Firmware \u0026amp; PCB Design # PCB design in KiCad and Breadboard protptyping; hardware debugging with oscilloscopes and VNAs Bare-metal firmware on ARM Cortex-A9 (Xilinx Vitis): AXI4-Lite memory-mapped register control Interrupt-driven architecture using hardware timers (TTC0 at 10 kHz) for real-time waveform generation PS–PL handshake protocol: flag/acknowledge signaling between ARM software and FPGA fabric FPGA \u0026amp; Digital Design # RTL design in VHDL on Xilinx Zynq-7010: datapath/control FSM architecture, custom AXI4-Lite slave IP, Vivado block design Datapath and control architecture: FSM-driven control word / status word separation Dual-port BRAM for cross-clock-domain buffering; acquisition clock to pixel clock Simulation and timing verification in ModelSim / Vivado RF \u0026amp; Electromagnetics # Full-wave EM simulation in Ansys HFSS antenna design, parametric sweeps, Optimetrics optimization Microstrip patch antenna design including impedance matching, coax. feed, circular polarization, and array configuration Circular array design with mutual coupling analysis and HFSS full-array verification PCB fabrication on Rogers substrates using CNC milling; S-parameter analysis via VNA measurement Tools \u0026amp; Platforms # Category Tools Software C, C++, MATLAB, VHDL, Verilog, Python FPGA Xilinx Vivado, Vitis, Quartus Prime Embedded/PCB Microchip MPLab, GCC toolchain, KiCad EM Simulation Ansys HFSS Tools Git, GitHub, Docker, Wireshark OS Windows, Ubuntu Linux Experience #Electrical Engineer — JHU Applied Physics Lab #July 2026 – Present\nDesigning low latency DSP pipeline in systems-level software to interface with software-defined radio.\nEmbedded Systems and Digital Logic Lab Assistant #Jan. 2026 – May 2026\nIn Embedded Systems, taught students C programming with MPLab IDE as well as MCU debugging with oscilloscopes. In Digital Logic, helped students with Verilog FPGA programming in Quartus, finite state machines, and testbenching.\nMachine Learning/Data Science Teaching Assistant #August 2025 – December 2025\nHelping students with Jupyter Notebooks and ML algorithms for real-world applications.\nSoftware Engineer Intern — JHU Applied Physics Lab #May 2025 – August 2025\nDeveloped agentic AI pipeline for Retrieval-Augmented Generation (RAG). Designed and simulated LPI/LPD digital spread spectrum communication systems like FHSS and DSSS. Built autonomous drone tracking system with on board computer vision and control.\nData Science Intern — Pan American Energy #May 2024 – August 2024\nDesigned an algorithm and Python tool to aid with oil field drilling locations using dynamic time warping with electric resistivity profiles.\nEducation #Colorado School of Mines #M.S. Electrical Engineering — May 2026\nB.S. Computer Science (Magna Cum Laude) — May 2025\nB.S. Mechanical Engineering (Magna Cum Laude) — May 2025\nOrganizations: Tau Beta Pi, SHPE, Boettcher Scholarship, Kappa Sigma\nContact Info: Connect on LinkedIn and/or Github\n","date":null,"permalink":"https://scast3.github.io/about/","section":"About","summary":"\u003ctable\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003cimg src=\"surf.jpeg\" height=\"300\"\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cimg src=\"hike.jpg\" height=\"300\"\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\u003cp\u003eHello, I am Santiago!\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;m currently an Electrical Engineer at JHU Applied Physics Lab working in the area of Communication Systems. My professional and academic work has been in the wireless PHY layer from receiver algorithm design to real-time DSP pipeline implementation in HDL and system-level software. Throughout my career, I have been passionate about tackling low latency and resource-constrained design challenges.\u003c/p\u003e\n\u003cp\u003eI have a master\u0026rsquo;s degree in Electrical Engineering from Colorado School of Mines and dual bachelor\u0026rsquo;s degrees in Computer Science and Mechanical Engineering.\u003c/p\u003e","title":"About"},{"content":"","date":null,"permalink":"https://scast3.github.io/categories/","section":"Categories","summary":"","title":"Categories"}]