<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Register-Map on Corebaseit — POS · EMV · Payments · AI · Telecommunications</title><link>https://corebaseit.com/tags/register-map/</link><description>Recent content in Register-Map on Corebaseit — POS · EMV · Payments · AI · Telecommunications</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><managingEditor>contact@corebaseit.com (Vincent Bevia)</managingEditor><webMaster>contact@corebaseit.com (Vincent Bevia)</webMaster><lastBuildDate>Fri, 12 Jun 2026 10:00:00 +0200</lastBuildDate><atom:link href="https://corebaseit.com/tags/register-map/index.xml" rel="self" type="application/rss+xml"/><item><title>From MATLAB to metal: 6 hard-won lessons in SoC integration</title><link>https://corebaseit.com/corebaseit_posts/from-matlab-to-metal-soc-integration-lessons/</link><pubDate>Fri, 12 Jun 2026 10:00:00 +0200</pubDate><author>contact@corebaseit.com (Vincent Bevia)</author><guid>https://corebaseit.com/corebaseit_posts/from-matlab-to-metal-soc-integration-lessons/</guid><description>&lt;p>The PHY algorithm passes every simulation in MATLAB. The Bit Error Rate (BER) curve hits the theoretical bound. The Error Vector Magnitude (EVM) budget has margin. The golden model is signed off. Then the design goes onto silicon, and the system fails to lock. Or it locks, but sits on a BER floor that no amount of loop-gain tuning can break through.&lt;/p>
&lt;p>I have seen this pattern enough times to know where it comes from. The gap between a floating-point MATLAB model and a working SoC is not one problem. It is a stack of them: fixed-point quantization, compiler assumptions about memory visibility, register interface timing, coefficient update atomicity. Each is individually tractable. Together, they form a discipline that rarely shows up in a single textbook or course.&lt;/p>
&lt;p>These are six lessons from that discipline. All of them cost real silicon time to learn.&lt;/p>
&lt;p style="text-align: center;">
&lt;img src="https://corebaseit.com/diagrams/algorithm-to-silicon.png" alt="Algorithm to silicon: the SoC development pipeline from MATLAB golden reference through C firmware, register map contract, to Python stage-by-stage validation" style="max-width: 900px; width: 100%;" />
&lt;/p>
&lt;h2 id="languages-are-a-pipeline-not-a-resume-line">Languages are a pipeline, not a resume line
&lt;/h2>&lt;p>A common mistake is treating MATLAB, C/C++, and Python as separate skills. In practice, they are stages in one verification pipeline:&lt;/p>
&lt;p>MATLAB defines the golden reference: the algorithm, the expected BER curve, the coefficient sets, the input/output vectors. C implements the firmware-facing datapath and hardware register interface. C++ provides simulation infrastructure and reusable DSP blocks (RAII for resource management, templates for parameterized filter stages) without obscuring the hardware cost model. Python runs regression harnesses, BER sweeps, and sample-by-sample comparisons between hardware captures and MATLAB references.&lt;/p>
&lt;p>The flow looks like this:&lt;/p>
&lt;p>Reference Model → Vector Export → C/C++ Implementation → Register Interface → Hardware Capture → Python Comparison → Debug → Spec Update.&lt;/p>
&lt;p>That last step matters more than it usually gets credit for. The loop only closes when the debug results feed back into the specification. Specifications that are not updated after bring-up become a liability on the next tape-out. The documented behavior and the actual silicon behavior quietly diverge, and the next team inherits assumptions that no longer match the hardware they are building on.&lt;/p>
&lt;p>Each language handles a different fidelity level and a different verification role. Treating them as isolated tools breaks the chain of evidence between &amp;ldquo;mathematically correct&amp;rdquo; and &amp;ldquo;physically correct on this die.&amp;rdquo;&lt;/p>
&lt;h2 id="the-register-map-is-a-contract-not-a-header-file">The register map is a contract, not a header file
&lt;/h2>&lt;p>The boundary between hardware and firmware is the decision with the highest cost of change in SoC design. Once the chip is taped out, the register interface is fixed. Firmware can be patched. Silicon cannot.&lt;/p>
&lt;p>Hardware should own the sample-rate and symbol-rate datapath: FFT/IFFT engines, FIR filters, FEC datapaths, timing recovery (the Timing Error Detector and interpolator). These run at rates where firmware cannot keep up.&lt;/p>
&lt;p>Firmware should own intelligence and policy: loop sequencing, gain updates (Kp/Ki in a PLL, step-size μ in an equalizer), power-state transitions, and calibration routines. These change across operating modes, channel conditions, and device revisions.&lt;/p>
&lt;p>The architectural discipline is in what goes where and how the two sides communicate through registers.&lt;/p>
&lt;p>Mixing control (read/write) and status (read-only) fields in the same register invites Read-Modify-Write (RMW) hazards. The firmware reads the register, modifies a control bit, and writes it back. Between the read and the write, the hardware sets a status bit. The write overwrites it. The event is lost. The fix is structural: keep control and status in separate registers. This is standard practice in ARM AMBA peripheral designs and is explicitly recommended in the Accellera SystemRDL 2.0 register description methodology [1][2].&lt;/p>
&lt;p>For sticky status bits (saturation flags, overflow indicators, lock-loss events), use Write-1-to-Clear (W1C) semantics. The hardware sets the bit on the event. The firmware clears it by writing a 1 to that bit position. The event persists until explicitly acknowledged. Writing 0 has no effect, which eliminates the RMW race entirely for that field.&lt;/p>
&lt;h2 id="volatile-is-necessary-and-not-always-sufficient">&lt;code>volatile&lt;/code> is necessary, and not always sufficient
&lt;/h2>&lt;p>A classic bring-up surprise: the firmware reads a hardware register and gets stale data. The engineer stares at the waveform, confirms the hardware updated the value two microseconds ago, and the C code is still reading the old one.&lt;/p>
&lt;p>The cause is compiler optimization. The C standard permits a conforming compiler to cache the value of a memory read if no intervening &lt;em>software&lt;/em> operation modifies that address (ISO/IEC 9899:2011, §6.7.3) [3]. On an SoC, hardware modifies memory-mapped registers asynchronously to the CPU. The compiler has no visibility into that.&lt;/p>
&lt;p>The &lt;code>volatile&lt;/code> qualifier instructs the compiler: do not cache reads or defer writes to this address. Every access must produce an actual bus transaction.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#define IQ_STATUS (*(volatile uint32_t *)0x40001004)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">uint32_t&lt;/span> status &lt;span style="color:#f92672">=&lt;/span> IQ_STATUS; &lt;span style="color:#75715e">// guaranteed load from 0x40001004
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That handles the compiler. It does not handle the memory system.&lt;/p>
&lt;p>On multi-core SoCs or any system where caches sit between the CPU and the peripheral bus, &lt;code>volatile&lt;/code> alone is insufficient. You also need cache management (invalidate before reads, clean/flush after writes) or the memory-mapped I/O region must be configured as Device or Strongly-Ordered memory in the MMU/MPU page tables [4]. On ARM Cortex-M class cores without data caches, &lt;code>volatile&lt;/code> is typically the whole story. On Cortex-A cores running Linux, the kernel&amp;rsquo;s &lt;code>readl()&lt;/code>/&lt;code>writel()&lt;/code> accessors bundle the volatile semantics with the required memory barriers and, on cached systems, operate through regions already mapped as Device memory.&lt;/p>
&lt;p>The short version: &lt;code>volatile&lt;/code> is the floor. Whether it is also the ceiling depends on the memory hierarchy between your CPU and your peripheral bus.&lt;/p>
&lt;h2 id="fixed-point-success-is-won-in-the-accumulator">Fixed-point success is won in the accumulator
&lt;/h2>&lt;p>Moving from MATLAB&amp;rsquo;s double-precision floats to C&amp;rsquo;s fixed-point integers is where many BER floors originate. The arithmetic is straightforward. The discipline is in managing bit growth, rounding bias, and overflow behavior.&lt;/p>
&lt;p>Multiplying two Q1.15 values produces a Q2.30 result. In an N-tap FIR filter, the accumulator sums N such products, adding \(\lceil \log_2 N \rceil\) bits of potential growth. For a 64-tap filter:&lt;/p>
\[
\text{ACC\_WIDTH} \geq 16 + 16 + \lceil \log_2(64) \rceil = 38 \text{ bits}
\]&lt;p>Using &lt;code>int64_t&lt;/code> for the accumulator provides 64 bits, leaving 26 bits of headroom above the minimum. That margin absorbs coefficient sets you have not tested yet and input sequences that hit worst-case correlation patterns. On Xilinx UltraScale+ devices, the DSP48E2 slice provides a 48-bit accumulator natively [5]; on Intel FPGAs, the variable-precision DSP block goes up to 64 bits in accumulation mode.&lt;/p>
&lt;p>When scaling the accumulator back to Q1.15 (right-shift by 15), truncation introduces a negative DC bias of half an LSB on average. Over a long integration, that bias accumulates. In a feedback loop (equalizer, PLL loop filter), it shows up as a carrier offset or a BER floor that does not respond to gain adjustments. The fix is to add a rounding constant before the shift:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">int16_t&lt;/span> result &lt;span style="color:#f92672">=&lt;/span> (&lt;span style="color:#66d9ef">int16_t&lt;/span>)((acc &lt;span style="color:#f92672">+&lt;/span> (&lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#f92672">&amp;lt;&amp;lt;&lt;/span> &lt;span style="color:#ae81ff">14&lt;/span>)) &lt;span style="color:#f92672">&amp;gt;&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">15&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This implements round-half-up and removes the systematic bias. Oppenheim and Schafer treat the statistical properties of roundoff noise in detail [6, Ch. 6].&lt;/p>
&lt;p>Saturation is the other non-negotiable. If a signed 16-bit value overflows, two&amp;rsquo;s complement wraps: +32768 becomes −32768. In a signal chain, that is a full-scale phase inversion — a single sample can corrupt an entire symbol or frame. The fix:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">static&lt;/span> &lt;span style="color:#66d9ef">inline&lt;/span> &lt;span style="color:#66d9ef">int16_t&lt;/span> &lt;span style="color:#a6e22e">sat16&lt;/span>(&lt;span style="color:#66d9ef">int32_t&lt;/span> x) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> (x &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">32767&lt;/span>) &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#ae81ff">32767&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> (x &lt;span style="color:#f92672">&amp;lt;&lt;/span> &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">32768&lt;/span>) &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">32768&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> (&lt;span style="color:#66d9ef">int16_t&lt;/span>)x;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>On ARM Cortex-M4 and later, the &lt;code>SSAT&lt;/code> instruction performs signed saturation in a single cycle [4]. In MATLAB, saturation does not exist by default. If you are using the Fixed-Point Designer toolbox, you must explicitly set &lt;code>OverflowAction&lt;/code> to &lt;code>'Saturate'&lt;/code> on your &lt;code>fi()&lt;/code> objects. If your golden model silently wraps on overflow, the comparison against hardware is testing the wrong reference.&lt;/p>
&lt;h2 id="atomic-register-commits-prevent-transient-nightmares">Atomic register commits prevent transient nightmares
&lt;/h2>&lt;p>Firmware updates a 2×2 IQ correction matrix. Four coefficients: C11, C12, C21, C22. Firmware writes them sequentially over an APB bus. Between the write to C12 and the write to C21, the hardware processes a block of samples using two new coefficients and two old ones. The constellation rotates for a few symbols, then recovers when the remaining writes land. The BER measurement shows an intermittent spike that looks like a calibration instability.&lt;/p>
&lt;p>I have spent more debug hours on this failure mode than I care to admit. It is particularly insidious because it is intermittent, timing-dependent, and the steady-state behavior after all writes complete looks correct.&lt;/p>
&lt;p>The fix is shadow registers with an atomic commit. Firmware writes new coefficients to shadow locations that the active datapath does not read:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Address&lt;/th>
&lt;th>Register&lt;/th>
&lt;th>Access&lt;/th>
&lt;th>Function&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>0x0000&lt;/td>
&lt;td>IQ_CTRL&lt;/td>
&lt;td>R/W&lt;/td>
&lt;td>ENABLE\[0\], MODE\[2:1\]&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x0004&lt;/td>
&lt;td>IQ_STATUS&lt;/td>
&lt;td>R/O&lt;/td>
&lt;td>LOCKED\[0\], SAT\[1\] (W1C)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x0010&lt;/td>
&lt;td>IQ_C11_SHADOW&lt;/td>
&lt;td>R/W&lt;/td>
&lt;td>Coefficient C11, Q2.14&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x0014&lt;/td>
&lt;td>IQ_C12_SHADOW&lt;/td>
&lt;td>R/W&lt;/td>
&lt;td>Coefficient C12, Q2.14&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x0018&lt;/td>
&lt;td>IQ_C21_SHADOW&lt;/td>
&lt;td>R/W&lt;/td>
&lt;td>Coefficient C21, Q2.14&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x001C&lt;/td>
&lt;td>IQ_C22_SHADOW&lt;/td>
&lt;td>R/W&lt;/td>
&lt;td>Coefficient C22, Q2.14&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x0020&lt;/td>
&lt;td>IQ_COMMIT&lt;/td>
&lt;td>W/O&lt;/td>
&lt;td>Loads all shadow → active at next symbol boundary&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>A write to &lt;code>IQ_COMMIT&lt;/code> triggers the hardware to transfer all four shadow registers into the active datapath simultaneously, aligned to the next symbol or sample clock boundary. The datapath never sees a partial coefficient set.&lt;/p>
&lt;p>This pattern applies to any multi-register parameter group: equalizer tap sets, AGC gain tables, NCO frequency words, anything where a partial update produces a transient that corrupts the signal chain.&lt;/p>
&lt;h2 id="debugging-by-bisection">Debugging by bisection
&lt;/h2>&lt;p>When the BER is wrong, comparing the final output against the golden model tells you &lt;em>that&lt;/em> something failed. It does not tell you &lt;em>where&lt;/em>. On a signal chain with six or eight processing stages, that distinction is the difference between hours of debug and weeks.&lt;/p>
&lt;p>The method that works is bisection. Place capture buffers at internal tap points along the datapath: post-ADC, post-AGC, post-channel-filter, post-FFT, post-equalizer, pre-demapper. Firmware reads each capture buffer into memory. Python compares the captured vectors, sample by sample, against the corresponding intermediate outputs from the MATLAB golden model.&lt;/p>
&lt;p>The divergence point is the stage where the implementation first departs from the reference. Upstream stages match; this stage does not. The fault is between those two tap points. The search space collapses from &amp;ldquo;the entire SoC&amp;rdquo; to one processing block.&lt;/p>
&lt;p>In practice, these capture buffers become permanent regression infrastructure. After any firmware or RTL change, the same tap-point comparison runs automatically. If a stage regresses, the CI pipeline flags the specific block before anyone looks at the overall BER curve. This turns debugging from opinion into measurement.&lt;/p>
&lt;p>One practical note: the capture buffers consume block RAM (on FPGA) or SRAM area (on ASIC). Budget for them early in the floorplan. Removing debug infrastructure to save area and then needing it during bring-up is a trade you make only once.&lt;/p>
&lt;h2 id="the-discipline-underneath">The discipline underneath
&lt;/h2>&lt;p>SoC integration is the work of making decisions that are hard to reverse, with as much evidence as you can gather before they become permanent. The register map freezes at tape-out. The fixed-point format freezes when RTL is signed off. The coefficient update scheme freezes when the micro-architecture is locked.&lt;/p>
&lt;p>The programming side of this work is not separate from the algorithm. It is the mechanism by which the algorithm becomes something that ships. The pipeline from MATLAB to C to silicon is a validation chain. Every link either builds confidence in the design or hides a failure that surfaces during bring-up, when the cost of finding it is highest.&lt;/p>
&lt;p>If the register boundary, the quantization scheme, and the update atomicity are right, bring-up is calibration. If any of them are wrong, bring-up is archaeology.&lt;/p>
&lt;h2 id="references">References
&lt;/h2>\[1\]&lt;p> ARM, &lt;em>AMBA APB Protocol Specification&lt;/em>, ARM IHI 0024E, Rev 2.0, 2021.&lt;/p>
\[2\]&lt;p> Accellera Systems Initiative, &lt;em>SystemRDL 2.0 Register Description Language&lt;/em>, 2018.&lt;/p>
\[3\]&lt;p> ISO/IEC 9899:2011, &lt;em>Programming Languages — C&lt;/em> (C11 Standard), §6.7.3 (Type qualifiers).&lt;/p>
\[4\]&lt;p> ARM, &lt;em>Cortex-A Series Programmer&amp;rsquo;s Guide for ARMv8-A&lt;/em>, ARM DEN0024A — Device memory ordering, barriers, and DSP/SIMD saturation instructions.&lt;/p>
\[5\]&lt;p> AMD/Xilinx, &lt;em>UltraScale Architecture DSP Slice User Guide (UG579)&lt;/em> — DSP48E2 accumulator width and cascade paths.&lt;/p>
\[6\]&lt;p> A. V. Oppenheim and R. W. Schafer, &lt;em>Discrete-Time Signal Processing&lt;/em>, 3rd ed., Pearson, 2010.&lt;/p>
\[7\]&lt;p> J. G. Proakis and M. Salehi, &lt;em>Digital Communications&lt;/em>, 5th ed., McGraw-Hill, 2008.&lt;/p>
\[8\]&lt;p> R. G. Lyons, &lt;em>Understanding Digital Signal Processing&lt;/em>, 3rd ed., Pearson, 2011.&lt;/p>
&lt;h2 id="further-reading">Further reading
&lt;/h2>&lt;ul>
&lt;li>&lt;a class="link" href="https://corebaseit.com/posts/lms-adaptive-filters-and-sgd-the-continuous-thread/" >I spent years on adaptive filters. I was already training neural networks.&lt;/a> — the continuous thread from LMS to SGD&lt;/li>
&lt;li>&lt;a class="link" href="https://corebaseit.com/posts/fir-filter-fpga-manual-vs-ai-workflows/" >FIR filter design on FPGA: manual engineering vs AI-assisted workflows&lt;/a> — fixed-point discipline and verification in RTL&lt;/li>
&lt;/ul></description></item></channel></rss>