Compare commits

..

14 Commits

Author SHA1 Message Date
Jason 27b55f37dc Merge pull request #108 from NawfalMotii79/fix/adar1000-channel-rotation
fix(adar1000): correct 1-based channel indexing in setters (issue #90)
2026-04-18 06:00:52 +03:00
Jason 582476fa0d fix(adar1000): correct 1-based channel indexing in setters (issue #90)
The four channel-indexed ADAR1000 setters (adarSetRxPhase, adarSetTxPhase,
adarSetRxVgaGain, adarSetTxVgaGain) computed their register offset as
`(channel & 0x03) * stride`, which silently aliased CH4 (channel=4 ->
mask=0) onto CH1 and shifted CH1..CH3 by one. The API contract (1-based
CH1..CH4) is documented in ADAR1000_AGC.cpp:76 and matches the ADI
datasheet; every existing caller already passes `ch + 1`.

Fix: subtract 1 before masking -- `((channel - 1) & 0x03) * stride` --
and reject `channel < 1 || channel > 4` early with a DIAG message so a
future stale 0-based caller fails loudly instead of writing to CH4.

Adds TestTier1Adar1000ChannelRegisterRoundTrip (9 tests) which closes
the loop independently of the driver:
  - parses the ADI register map directly from ADAR1000_Manager.h,
  - verifies the datasheet stride invariants (gain=1, phase=2),
  - auto-discovers every C++ TU under MCU_LIB_DIR / MCU_CODE_DIR so a
    new caller cannot silently escape the round-trip check,
  - asserts every caller's channel argument evaluates to {1,2,3,4} for
    ch in {0,1,2,3} (catches bare 0-based or literal-0 callers at CI
    time before the runtime bounds-check would silently drop them),
  - round-trips each (caller, ch) through the helper arithmetic and
    checks the final address equals REG_CH{ch+1}_*.

Adversarially validated: reverting any one helper, all four helpers,
corrupting the parsed register map, injecting a bare-ch caller, and
auto-discovering a literal-0 caller in a fresh TU each cause the
expected (and only the expected) test to fail.

Stacked on fix/adar1000-vm-tables (PR #107).
2026-04-18 06:39:07 +05:45
Jason 7c91a3e0b9 fix(adar1000): populate VM_I/VM_Q phase tables; remove dead VM_GAIN
The ADAR1000 vector-modulator I/Q lookup tables VM_I[128] and VM_Q[128]
were declared but defined as empty initialiser lists since the first
commit (5fbe97f). Every call to adarSetRxPhase / adarSetTxPhase therefore
wrote (I=0x00, Q=0x00) to registers 0x21/0x23 (Rx) and 0x32/0x34 (Tx)
regardless of the requested phase state, leaving beam steering completely
non-functional in firmware.

This commit:

* Populates VM_I[128] and VM_Q[128] from ADAR1000 datasheet Rev. B
  Tables 13-16 (p.34) on a uniform 2.8125 deg grid (360 / 128 states).
  Byte format: bits[7:6] reserved 0, bit[5] polarity (1 = positive
  lobe), bits[4:0] 5-bit unsigned magnitude - exactly as specified.
* Removes VM_GAIN[128] declaration and (empty) definition. The
  ADAR1000 has no separate VM gain register; per-channel VGA gain is
  set via CHx_RX_GAIN (0x10-0x13) / CHx_TX_GAIN (0x1C-0x1F) by
  adarSetRxVgaGain / adarSetTxVgaGain. VM_GAIN was never populated,
  never read anywhere in the firmware, and its presence falsely
  suggested a missing scaling step in the signal path.
* Adds 9_Firmware/tests/cross_layer/adar1000_vm_reference.py: an
  independently-derived ground-truth module containing the full
  datasheet table plus byte-format / uniform-grid / quadrant-symmetry
  / cardinal-point invariant checkers and a tolerant C array parser.
* Adds TestTier2Adar1000VmTableGroundTruth (9 tests) to
  test_cross_layer_contract.py, including a tokenising C/C++
  comment+string stripper used by the VM_GAIN reintroduction guard,
  and an adversarial self-test that corrupts one byte and asserts
  the comparison detects it (defends against silent bypass via
  future fixture/parser refactors).

Adversarially validated: removing the firmware definitions, flipping
a single byte, or reintroducing VM_GAIN as code each cause the suite
to fail; restoring causes it to pass. VM_GAIN appearing inside string
literals or comments correctly does NOT trip the guard.

Closes the empty-table half of the ADAR1000 phase-control bug class.
The separate channel-rotation issue (#90) will be addressed in a
follow-up PR.

Refs: 7_Components Datasheets and Application notes/ADAR1000.pdf
      Rev. B Tables 13-16 p.34
2026-04-18 02:02:07 +05:45
Jason fd6cff5b2b Merge pull request #102 from JJassonn69/chore/sync-main-into-develop
chore: sync main → develop (schematic updates + README + project doc)
2026-04-17 19:31:31 +03:00
Jason 964f1903f3 chore: sync main → develop (schematic updates, README, project doc)
Brings in main-only commits that never reached develop:
  754d919 Added silk screen and headers description (MainBoard .brd)
  0443516 Added thermal vias (RF_PA .brd)
  5fbe051 Added ABAC INDUSTRY web site (Project_Description.docx)
  12b549d / 5d5e9ff Merge PR #101: README BOM sensor counts fix

No conflicts expected: develop has not touched any of these paths.
2026-04-17 22:12:26 +05:45
Jason 12b549dafb Merge pull request #101 from JJassonn69/fix/readme-bom-sensor-counts
docs(readme): correct STM32 peripheral counts and locations to match production BOM
2026-04-17 17:21:59 +03:00
Jason 5d5e9ff297 docs(readme): correct BOM sensor counts and locations
The STM32 peripheral list in the README disagreed with the production
BOM (4_7_Production Files/Gerber_Main_Board/RADAR_Main_Board_BOM_csv)
and with the firmware (9_1_Microcontroller/.../main.cpp). Corrections
based on origin/main commit 754d919:

- ADS7830 Idq ADCs: placed on the Main Board (U88 @ 0x48, U89 @ 0x4A),
  not on the Power Amplifier Boards. Added the INA241A3 (x50) and 5 mOhm
  shunt detail that completes the current-sense chain.
- DAC5578 Vg DACs: placed on the Main Board (U7 @ 0x48, U69 @ 0x49),
  not on the Power Amplifier Boards. Noted closed-loop Idq calibration
  at boot (main.cpp powerUpSequence).
- Temperature sensors: 1x ADS7830 (U10) with 8 single-ended channels
  reading 8 thermistors -- not 8 separate ADS7830 chips. Cooling is a
  single GPIO (EN_DIS_COOLING), bang-bang, not PWM.
- GPS: reflect the UM982 driver merged in #79 and its role in
  per-detection position tagging beyond map centering.

Counts now match the 3x ADS7830 / 2x DAC5578 / 16x INA241A3 population
in the production BOM.
2026-04-17 20:04:01 +05:45
NawfalMotii79 754d919e44 Added silk screen and headers description 2026-04-16 23:48:23 +01:00
NawfalMotii79 0443516cc9 Added thermal vias 2026-04-16 23:47:24 +01:00
NawfalMotii79 5fbe0513b5 Added ABAC INDUSTRY web site 2026-04-16 23:46:08 +01:00
Jason c3db8a9122 Merge pull request #96 from joyshmitz/chore/remove-dead-adar1000-c-api
chore(mcu): remove dead C-style adar1000 driver
2026-04-16 23:51:22 +03:00
Jason ec8256e25a Merge pull request #95 from joyshmitz/test/agc-debounce-enforce
test(cross-layer): enforce 2-frame DIG_6 debounce guard on outerAgc.enabled (follow-up to #93)
2026-04-16 23:42:12 +03:00
Serhii 8e1b3f22d2 chore(mcu): remove dead C-style adar1000 driver
The firmware uses the C++ ADAR1000_Manager class exclusively. The C-style
driver pair (adar1000.c, 693 LoC; adar1000.h, 294 LoC) has no external
call sites:

  grep -rn "Adar_Set|Adar_Read|Adar_Write|Adar_Soft" 9_Firmware
  grep -rn "AdarDevice|AdarBiasCurrents|AdarDeviceInfo" 9_Firmware

Both return hits only inside adar1000.c/h themselves. ADAR1000_Manager.h
has its own copies of REG_CH1_*, REG_INTERFACE_CONFIG_A, etc. and does
not include adar1000.h. main.cpp had a lone #include "adar1000.h" but
referenced no symbols from it; the REG_* macros it uses resolve through
ADAR1000_Manager.h on the next line.

No behaviour change: the deleted code was unreachable.

Side note on #90: adar1000.c contained a second copy of the
REG_CH1_* + (channel & 0x03) channel-rotation pattern tracked in #90
(lines 349, 397-398, 472, 520-521). This commit does not fix #90 --
the live path in ADAR1000_Manager.cpp still needs the channel-index
fix -- but it removes the dormant copy so the bug has one less place
to hide.

Verification:
- 9_Firmware/9_1_Microcontroller/tests: make clean && make -> all passing
  (51/51 UM982 GPS, 24/24 driver, 13/13 ADAR1000_AGC, bugs #1-15, Gap-3
  fixes 1-5, safety fixes)
- 9_Firmware/tests/cross_layer: 29 passed
- grep -rn "adar1000\.h|adar1000\.c|Adar_|AdarDevice" 9_Firmware: 0 hits
2026-04-16 22:12:23 +03:00
Serhii 15ae940be5 test(cross-layer): enforce 2-frame DIG_6 debounce guard on outerAgc.enabled
PR #93 added a 2-frame confirmation debounce so a single-sample GPIO
glitch cannot flip MCU outer-loop AGC state. The debounce is load-bearing
for the "prevents a single-sample glitch" guarantee in the PR body, but
no existing test enforces its structure — test_mcu_reads_dig6_before_agc_gate
only checks that HAL_GPIO_ReadPin(FPGA_DIG6, ...) and `outerAgc.enabled =`
appear somewhere in main.cpp, which a naive direct assignment would still
pass.

Add test_mcu_dig6_debounce_guards_enable_assignment to
TestTier1AgcCrossLayerInvariant, verifying four structural invariants of
the debounce:

  1. Current DIG_6 sample captured in a local variable
  2. Static previous-frame variable defaulting to false (matches FPGA
     boot: host_agc_enable resets 0)
  3. outerAgc.enabled assignment gated by `now == prev`
  4. Previous-frame variable advanced each frame

Verified test fails on a naive patch that removes the guard and passes
on the current PR #93 implementation. Full cross-layer suite stays at
0 failures (36/36 pass locally).
2026-04-16 21:29:37 +03:00
11 changed files with 3456 additions and 1158 deletions
Binary file not shown.
@@ -550,7 +550,7 @@
<text x="3.085225" y="81.68279375" size="1.778" layer="51">GND</text>
<text x="2.3" y="53.85" size="1.778" layer="51">GND</text>
<text x="3.336225" y="42.247028125" size="1.778" layer="51">GND</text>
<text x="2.25" y="11.75" size="1.778" layer="51">GND</text>
<text x="2.99881875" y="12.58869375" size="1.778" layer="51">GND</text>
<text x="21.75" y="12.15" size="1.778" layer="51" rot="R90">GND</text>
<text x="37.45" y="10.05" size="1.778" layer="51" rot="R90">GND</text>
<text x="60.5" y="9.4" size="1.778" layer="51" rot="R90">GND</text>
@@ -589,11 +589,11 @@
<text x="248.95" y="49.2" size="1.778" layer="51" rot="R180">GND</text>
<text x="248.85" y="66.55" size="1.778" layer="51" rot="R180">GND</text>
<text x="248.8" y="82.9" size="1.778" layer="51" rot="R180">GND</text>
<text x="256.35" y="101.95" size="1.778" layer="51" rot="R180">GND</text>
<text x="249.4" y="112.5" size="1.778" layer="51" rot="R180">GND</text>
<text x="253.964015625" y="102.099125" size="1.778" layer="51" rot="R180">GND</text>
<text x="249.054865625" y="112.111771875" size="1.778" layer="51" rot="R180">GND</text>
<text x="237.75" y="280.1" size="1.778" layer="51" rot="R270">GND</text>
<text x="199.75" y="273.55" size="1.778" layer="51" rot="R270">GND</text>
<text x="188.45" y="272.75" size="1.778" layer="51" rot="R270">GND</text>
<text x="188.539503125" y="273.018421875" size="1.778" layer="51" rot="R270">GND</text>
<text x="177.95" y="272.75" size="1.778" layer="51" rot="R270">GND</text>
<text x="113.4" y="281.65" size="1.778" layer="51" rot="R270">GND</text>
<text x="2.992190625" y="248.58331875" size="1.778" layer="51">GND</text>
@@ -635,13 +635,13 @@
<wire x1="161.6" y1="158.7" x2="156.95" y2="163.4" width="2.54" layer="29"/>
<wire x1="170.1" y1="150.2" x2="165.45" y2="154.9" width="2.54" layer="29"/>
<text x="125.137784375" y="269.740521875" size="1.778" layer="51" rot="R90">+5V0_PA_1</text>
<text x="185.45" y="267.2" size="1.778" layer="51" rot="R90">-3V4</text>
<text x="196.5" y="267.4" size="1.778" layer="51" rot="R90">+3V4</text>
<text x="182.675396875" y="267.73684375" size="1.778" layer="51" rot="R90">-3V4</text>
<text x="193.277878125" y="266.86315625" size="1.778" layer="51" rot="R90">+3V4</text>
<text x="207.4" y="267.85" size="1.778" layer="51" rot="R90">-5V0_ADAR12</text>
<text x="188.75" y="289.05" size="1.3" layer="51" rot="R45">+3V3_ADAR12</text>
<text x="248.25" y="270.6" size="1.778" layer="51" rot="R90">+5V0_PA_2</text>
<text x="242.8" y="98.7" size="1.778" layer="51" rot="R180">+3V4</text>
<text x="242.9" y="106.65" size="1.778" layer="51" rot="R180">-3V4</text>
<text x="249.695853125" y="96.471690625" size="1.778" layer="51" rot="R180">+3V4</text>
<text x="249.232640625" y="104.692303125" size="1.778" layer="51" rot="R180">-3V4</text>
<text x="181.4" y="99.15" size="1.778" layer="51" rot="R270">-5V0_ADAR34</text>
<text x="185.3" y="75.15" size="1.778" layer="51" rot="R270">+3V3_ADAR34</text>
<text x="238.95" y="72.8" size="1.778" layer="51">+3V3_VDD_SW</text>
@@ -714,8 +714,8 @@
<text x="147.05" y="25.3" size="1.778" layer="51" rot="R180">CHAN14</text>
<text x="157.1" y="25.25" size="1.778" layer="51" rot="R180">CHAN15</text>
<text x="167.15" y="25.35" size="1.778" layer="51" rot="R180">CHAN16</text>
<text x="50.15" y="131.25" size="1.778" layer="51" rot="R180">SV1</text>
<text x="43.25" y="128.5" size="1.778" layer="51" rot="R270">VOLTAGE SEQUENCING</text>
<text x="51.802165625" y="131.052934375" size="1.778" layer="51" rot="R180">SV1</text>
<text x="35.60243125" y="132.092775" size="1.778" layer="51" rot="R270">VOLTAGE SEQUENCING</text>
<text x="105.55" y="106.9" size="1.2" layer="51" rot="R90">AD9523_EEPROM_SEL</text>
<text x="107.2" y="101.85" size="1.2" layer="51" rot="R45">AD9523_STATUS0</text>
<text x="107.25" y="99.35" size="1.2" layer="51" rot="R45">STM32_MOSI4</text>
@@ -728,20 +728,19 @@
<text x="99.8" y="100.75" size="1.2" layer="51" rot="R225">STM32_MISO4</text>
<text x="99.8" y="103.4" size="1.2" layer="51" rot="R225">AD9523_STATUS1</text>
<text x="99.7" y="105.85" size="1.2" layer="51" rot="R225">GND</text>
<text x="68.7" y="82.55" size="1.778" layer="51">JP4</text>
<text x="64.25" y="73.85" size="1.778" layer="51" rot="R270">GND</text>
<text x="68.73355625" y="72.201796875" size="1.778" layer="51">JP4</text>
<text x="62.77508125" y="75.956934375" size="1" layer="51" rot="R225">GND</text>
<text x="56.95" y="82.75" size="1.778" layer="51">JP9</text>
<text x="37.85" y="78.6" size="1.778" layer="51" rot="R90">JP2</text>
<text x="43.95" y="88.9" size="1.778" layer="51">JP8</text>
<text x="29.1" y="93.2" size="1.778" layer="51">JP7</text>
<text x="21.75" y="85.35" size="1.778" layer="51">JP18</text>
<text x="45.798875" y="84.61879375" size="1.778" layer="51" rot="R180">JP2</text>
<text x="43.09716875" y="85.33433125" size="1.778" layer="51" rot="R90">JP8</text>
<text x="29.1" y="93.2" size="1.778" layer="51">IMU</text>
<text x="27.568784375" y="88.61074375" size="1.778" layer="51">JP18</text>
<text x="89.3" y="75.5" size="1.778" layer="51" rot="R180">JP13</text>
<text x="75.2" y="77" size="1.778" layer="51" rot="R270">GND</text>
<text x="69.6" y="74.1" size="1.778" layer="51" rot="R270">GND</text>
<text x="62.9" y="82.75" size="1.778" layer="51">JP10</text>
<text x="53.75" y="64.4" size="1.2" layer="51" rot="R45">STEPPER_CLK+</text>
<text x="43.9" y="78.65" size="1.778" layer="51" rot="R270">GND</text>
<text x="53.95" y="86.4" size="1.778" layer="51">GND</text>
<text x="62.1909375" y="71.621040625" size="1.778" layer="51">JP10</text>
<text x="54.996875" y="70.359128125" size="1.2" layer="51">STEPPER</text>
<text x="43.9" y="78.65" size="1.27" layer="51" rot="R270">GND</text>
<text x="52.61158125" y="88.897171875" size="1.016" layer="51" rot="R90">GND</text>
<text x="31.3" y="84.75" size="1.778" layer="51" rot="R270">GND</text>
<text x="40.45" y="95.9" size="1.778" layer="51" rot="R90">GND</text>
<rectangle x1="12.8295" y1="256.5735" x2="15.6235" y2="256.7005" layer="51"/>
@@ -5387,6 +5386,56 @@
<text x="122.221528125" y="146.5440625" size="1.27" layer="51" rot="R315">RX 3_4</text>
<text x="145.05015" y="114.518025" size="1.27" layer="51" rot="R45">RX 4_4</text>
<text x="150.25345625" y="4.79933125" size="5.4864" layer="51" font="vector">www.abac-industry.com</text>
<text x="47.269546875" y="127.64274375" size="1.27" layer="51" rot="R135">+1V0_FPGA</text>
<text x="47.220515625" y="125.152134375" size="1.27" layer="51" rot="R135">+1V8_FPGA</text>
<text x="47.270815625" y="122.549565625" size="1.27" layer="51" rot="R135">+3V3_FPGA</text>
<text x="47.317503125" y="119.8292125" size="1.27" layer="51" rot="R135">+5V0_ADAR</text>
<text x="47.30423125" y="117.319196875" size="1.27" layer="51" rot="R135">+3V3_ADAR12</text>
<text x="47.2552" y="114.8285875" size="1.27" layer="51" rot="R135">+3V3_ADAR34</text>
<text x="47.3055" y="112.22601875" size="1.27" layer="51" rot="R135">+3V3_ADTR</text>
<text x="47.3521875" y="109.505665625" size="1.27" layer="51" rot="R135">+3V3_SW</text>
<text x="47.262328125" y="107.0494875" size="1.27" layer="51" rot="R135">+3V3_VDD_SW</text>
<text x="47.262328125" y="104.6232625" size="1.27" layer="51" rot="R135">+5V0_PA1</text>
<text x="52.848896875" y="114.716634375" size="1.27" layer="51" rot="R315">GND</text>
<text x="52.897928125" y="117.20724375" size="1.27" layer="51" rot="R315">+3V3_CLOCK</text>
<text x="52.847628125" y="119.8098125" size="1.27" layer="51" rot="R315">+1V8_CLOCK</text>
<text x="52.800940625" y="122.530165625" size="1.27" layer="51" rot="R315">+5V5_PA</text>
<text x="52.8908" y="124.98634375" size="1.27" layer="51" rot="R315">+5V0_PA3</text>
<text x="52.8908" y="127.41256875" size="1.27" layer="51" rot="R315">+5V0_PA2</text>
<text x="52.866228125" y="112.238071875" size="1.27" layer="51" rot="R315">GND</text>
<text x="52.79689375" y="109.7075125" size="1.27" layer="51" rot="R315">GND</text>
<text x="52.7795625" y="107.038290625" size="1.27" layer="51" rot="R315">GND</text>
<text x="52.762228125" y="104.50773125" size="1.27" layer="51" rot="R315">GND</text>
<text x="37.741834375" y="95.9444" size="1.778" layer="51" rot="R90">+3V3</text>
<text x="43.11376875" y="95.9444" size="1.778" layer="51" rot="R90">SCL3</text>
<text x="45.64435" y="95.9888" size="1.778" layer="51" rot="R90">SDA3</text>
<text x="48.232090625" y="95.98181875" size="1.016" layer="51" rot="R90">MAG_DRDY</text>
<text x="50.801084375" y="95.879059375" size="1.016" layer="51" rot="R90">ACC_INT</text>
<text x="52.907659375" y="95.95613125" size="1.016" layer="51" rot="R90">GYR_INT</text>
<text x="54.502678125" y="92.739546875" size="1.778" layer="51">JP7</text>
<text x="30.45236875" y="78.6816375" size="1.778" layer="51" rot="R90">+3V3</text>
<text x="35.56853125" y="79.257065625" size="1.778" layer="51" rot="R90">SCL3</text>
<text x="38.227" y="78.789975" size="1.778" layer="51" rot="R90">SDA3</text>
<text x="39.282209375" y="78.488071875" size="1.27" layer="51" rot="R270">+3V3</text>
<text x="41.4419875" y="78.63334375" size="1.27" layer="51" rot="R270">STM32_SWCLK</text>
<text x="46.663971875" y="78.473509375" size="1.27" layer="51" rot="R270">STM32_SWDIO</text>
<text x="49.16839375" y="78.5267875" size="1.27" layer="51" rot="R270">STM32_NRST</text>
<text x="51.7793875" y="78.473509375" size="1.27" layer="51" rot="R270">STM32_SWO</text>
<text x="53.6100625" y="82.81805625" size="1.27" layer="51" rot="R315">GND</text>
<text x="53.75804375" y="77.6019375" size="1.27" layer="51" rot="R315">GND</text>
<text x="53.809425" y="80.29940625" size="1.27" layer="51" rot="R315">CW+</text>
<text x="53.520859375" y="75.467190625" size="1.27" layer="51" rot="R315">CLK+</text>
<text x="50.081" y="88.941571875" size="1.016" layer="51" rot="R90">RX5</text>
<text x="47.417228125" y="88.985971875" size="1.016" layer="51" rot="R90">TX5</text>
<text x="45.019834375" y="88.675175" size="1.016" layer="51" rot="R90">+3V3</text>
<text x="53.525646875" y="86.07393125" size="1.778" layer="51">GPS</text>
<text x="62.34479375" y="80.785540625" size="0.9" layer="51" rot="R45">EN/DIS_RFPA_VDD</text>
<text x="68.0472625" y="76.328084375" size="1" layer="51" rot="R225">GND</text>
<text x="67.5982" y="80.711553125" size="0.9" layer="51" rot="R45">EN/DIS_COOLING</text>
<text x="78.325053125" y="83.140434375" size="1.778" layer="51">ADF4382</text>
<text x="92.67903125" y="80.894575" size="1.016" layer="51">1</text>
<text x="92.77235625" y="78.2390125" size="1.016" layer="51">2</text>
<text x="73.362715625" y="77.945809375" size="1.016" layer="51">14</text>
</plain>
<libraries>
<library name="eagle-ltspice">
@@ -24576,8 +24625,8 @@ Your PCBWay Team
<vertex x="114" y="112" curve="-180"/>
</polygon>
<polygon width="0.254" layer="1" spacing="5.08">
<vertex x="258.75" y="116" curve="-180"/>
<vertex x="254.75" y="112" curve="-180"/>
<vertex x="258.9164" y="116.0208" curve="-180"/>
<vertex x="254.9164" y="112.0208" curve="-180"/>
</polygon>
<polygon width="0.254" layer="1" spacing="5.08">
<vertex x="260" y="300"/>
File diff suppressed because it is too large Load Diff
@@ -20,18 +20,71 @@ static const struct {
{ADAR_4_CS_3V3_GPIO_Port, ADAR_4_CS_3V3_Pin} // ADAR1000 #4
};
// Vector Modulator lookup tables
// ADAR1000 Vector Modulator lookup tables (128-state phase grid, 2.8125 deg step).
//
// Source: Analog Devices ADAR1000 datasheet Rev. B, Tables 13-16, page 34
// (7_Components Datasheets and Application notes/ADAR1000.pdf)
// Cross-checked against the ADI Linux mainline driver (GPL-2.0, NOT vendored):
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/
// drivers/iio/beamformer/adar1000.c (adar1000_phase_values[])
// The 128 byte values themselves are factual data from the datasheet and are
// not subject to copyright; only the ADI driver code is GPL.
//
// Byte format (per datasheet):
// bit [7:6] reserved (0)
// bit [5] polarity: 1 = positive lobe (sign(I) or sign(Q) >= 0)
// 0 = negative lobe
// bits [4:0] 5-bit unsigned magnitude (0..31)
// At magnitude=0 the polarity bit is physically meaningless; the datasheet
// uses POL=1 (e.g. VM_Q at 0 deg = 0x20, VM_I at 90 deg = 0x21).
//
// Index mapping is uniform: VM_I[k] / VM_Q[k] correspond to phase angle
// k * 360/128 = k * 2.8125 degrees. Callers index as VM_*[phase % 128].
const uint8_t ADAR1000Manager::VM_I[128] = {
// ... (same as in your original file)
0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3E, 0x3E, 0x3D, // [ 0] 0.0000 deg
0x3D, 0x3C, 0x3C, 0x3B, 0x3A, 0x39, 0x38, 0x37, // [ 8] 22.5000 deg
0x36, 0x35, 0x34, 0x33, 0x32, 0x30, 0x2F, 0x2E, // [ 16] 45.0000 deg
0x2C, 0x2B, 0x2A, 0x28, 0x27, 0x25, 0x24, 0x22, // [ 24] 67.5000 deg
0x21, 0x01, 0x03, 0x04, 0x06, 0x07, 0x08, 0x0A, // [ 32] 90.0000 deg
0x0B, 0x0D, 0x0E, 0x0F, 0x11, 0x12, 0x13, 0x14, // [ 40] 112.5000 deg
0x16, 0x17, 0x18, 0x19, 0x19, 0x1A, 0x1B, 0x1C, // [ 48] 135.0000 deg
0x1C, 0x1D, 0x1E, 0x1E, 0x1E, 0x1F, 0x1F, 0x1F, // [ 56] 157.5000 deg
0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1E, 0x1E, 0x1D, // [ 64] 180.0000 deg
0x1D, 0x1C, 0x1C, 0x1B, 0x1A, 0x19, 0x18, 0x17, // [ 72] 202.5000 deg
0x16, 0x15, 0x14, 0x13, 0x12, 0x10, 0x0F, 0x0E, // [ 80] 225.0000 deg
0x0C, 0x0B, 0x0A, 0x08, 0x07, 0x05, 0x04, 0x02, // [ 88] 247.5000 deg
0x01, 0x21, 0x23, 0x24, 0x26, 0x27, 0x28, 0x2A, // [ 96] 270.0000 deg
0x2B, 0x2D, 0x2E, 0x2F, 0x31, 0x32, 0x33, 0x34, // [104] 292.5000 deg
0x36, 0x37, 0x38, 0x39, 0x39, 0x3A, 0x3B, 0x3C, // [112] 315.0000 deg
0x3C, 0x3D, 0x3E, 0x3E, 0x3E, 0x3F, 0x3F, 0x3F, // [120] 337.5000 deg
};
const uint8_t ADAR1000Manager::VM_Q[128] = {
// ... (same as in your original file)
0x20, 0x21, 0x23, 0x24, 0x26, 0x27, 0x28, 0x2A, // [ 0] 0.0000 deg
0x2B, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x33, 0x34, // [ 8] 22.5000 deg
0x35, 0x36, 0x37, 0x38, 0x38, 0x39, 0x3A, 0x3A, // [ 16] 45.0000 deg
0x3B, 0x3C, 0x3C, 0x3C, 0x3D, 0x3D, 0x3D, 0x3D, // [ 24] 67.5000 deg
0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3C, 0x3C, 0x3C, // [ 32] 90.0000 deg
0x3B, 0x3A, 0x3A, 0x39, 0x38, 0x38, 0x37, 0x36, // [ 40] 112.5000 deg
0x35, 0x34, 0x33, 0x31, 0x30, 0x2F, 0x2E, 0x2D, // [ 48] 135.0000 deg
0x2B, 0x2A, 0x28, 0x27, 0x26, 0x24, 0x23, 0x21, // [ 56] 157.5000 deg
0x20, 0x01, 0x03, 0x04, 0x06, 0x07, 0x08, 0x0A, // [ 64] 180.0000 deg
0x0B, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x13, 0x14, // [ 72] 202.5000 deg
0x15, 0x16, 0x17, 0x18, 0x18, 0x19, 0x1A, 0x1A, // [ 80] 225.0000 deg
0x1B, 0x1C, 0x1C, 0x1C, 0x1D, 0x1D, 0x1D, 0x1D, // [ 88] 247.5000 deg
0x1D, 0x1D, 0x1D, 0x1D, 0x1D, 0x1C, 0x1C, 0x1C, // [ 96] 270.0000 deg
0x1B, 0x1A, 0x1A, 0x19, 0x18, 0x18, 0x17, 0x16, // [104] 292.5000 deg
0x15, 0x14, 0x13, 0x11, 0x10, 0x0F, 0x0E, 0x0D, // [112] 315.0000 deg
0x0B, 0x0A, 0x08, 0x07, 0x06, 0x04, 0x03, 0x01, // [120] 337.5000 deg
};
const uint8_t ADAR1000Manager::VM_GAIN[128] = {
// ... (same as in your original file)
};
// NOTE: a VM_GAIN[128] table previously existed here as a placeholder but was
// never populated and never read. The ADAR1000 vector modulator has no
// separate gain register: phase-state magnitude is encoded directly in
// bits [4:0] of the VM_I/VM_Q bytes above. Per-channel VGA gain is a
// distinct register (CHx_RX_GAIN at 0x10-0x13, CHx_TX_GAIN at 0x1C-0x1F)
// written with the user-supplied byte directly by adarSetRxVgaGain() /
// adarSetTxVgaGain(). Do not reintroduce a VM_GAIN[] array.
ADAR1000Manager::ADAR1000Manager() {
for (int i = 0; i < 4; ++i) {
@@ -815,11 +868,22 @@ void ADAR1000Manager::adarSetRamBypass(uint8_t deviceIndex, uint8_t broadcast) {
}
void ADAR1000Manager::adarSetRxPhase(uint8_t deviceIndex, uint8_t channel, uint8_t phase, uint8_t broadcast) {
// channel is 1-based (CH1..CH4) per API contract documented in
// ADAR1000_AGC.cpp and matching ADI datasheet terminology.
// Reject out-of-range early so a stale 0-based caller does not
// silently wrap to ((0-1) & 0x03) == 3 and write to CH4.
// See issue #90.
if (channel < 1 || channel > 4) {
DIAG("BF", "adarSetRxPhase: channel %u out of range [1..4], ignored", channel);
return;
}
uint8_t i_val = VM_I[phase % 128];
uint8_t q_val = VM_Q[phase % 128];
uint32_t mem_addr_i = REG_CH1_RX_PHS_I + (channel & 0x03) * 2;
uint32_t mem_addr_q = REG_CH1_RX_PHS_Q + (channel & 0x03) * 2;
// Subtract 1 to convert 1-based channel to 0-based register offset
// before masking. See issue #90.
uint32_t mem_addr_i = REG_CH1_RX_PHS_I + ((channel - 1) & 0x03) * 2;
uint32_t mem_addr_q = REG_CH1_RX_PHS_Q + ((channel - 1) & 0x03) * 2;
adarWrite(deviceIndex, mem_addr_i, i_val, broadcast);
adarWrite(deviceIndex, mem_addr_q, q_val, broadcast);
@@ -827,11 +891,16 @@ void ADAR1000Manager::adarSetRxPhase(uint8_t deviceIndex, uint8_t channel, uint8
}
void ADAR1000Manager::adarSetTxPhase(uint8_t deviceIndex, uint8_t channel, uint8_t phase, uint8_t broadcast) {
// channel is 1-based (CH1..CH4). See issue #90.
if (channel < 1 || channel > 4) {
DIAG("BF", "adarSetTxPhase: channel %u out of range [1..4], ignored", channel);
return;
}
uint8_t i_val = VM_I[phase % 128];
uint8_t q_val = VM_Q[phase % 128];
uint32_t mem_addr_i = REG_CH1_TX_PHS_I + (channel & 0x03) * 2;
uint32_t mem_addr_q = REG_CH1_TX_PHS_Q + (channel & 0x03) * 2;
uint32_t mem_addr_i = REG_CH1_TX_PHS_I + ((channel - 1) & 0x03) * 2;
uint32_t mem_addr_q = REG_CH1_TX_PHS_Q + ((channel - 1) & 0x03) * 2;
adarWrite(deviceIndex, mem_addr_i, i_val, broadcast);
adarWrite(deviceIndex, mem_addr_q, q_val, broadcast);
@@ -839,13 +908,23 @@ void ADAR1000Manager::adarSetTxPhase(uint8_t deviceIndex, uint8_t channel, uint8
}
void ADAR1000Manager::adarSetRxVgaGain(uint8_t deviceIndex, uint8_t channel, uint8_t gain, uint8_t broadcast) {
uint32_t mem_addr = REG_CH1_RX_GAIN + (channel & 0x03);
// channel is 1-based (CH1..CH4). See issue #90.
if (channel < 1 || channel > 4) {
DIAG("BF", "adarSetRxVgaGain: channel %u out of range [1..4], ignored", channel);
return;
}
uint32_t mem_addr = REG_CH1_RX_GAIN + ((channel - 1) & 0x03);
adarWrite(deviceIndex, mem_addr, gain, broadcast);
adarWrite(deviceIndex, REG_LOAD_WORKING, 0x1, broadcast);
}
void ADAR1000Manager::adarSetTxVgaGain(uint8_t deviceIndex, uint8_t channel, uint8_t gain, uint8_t broadcast) {
uint32_t mem_addr = REG_CH1_TX_GAIN + (channel & 0x03);
// channel is 1-based (CH1..CH4). See issue #90.
if (channel < 1 || channel > 4) {
DIAG("BF", "adarSetTxVgaGain: channel %u out of range [1..4], ignored", channel);
return;
}
uint32_t mem_addr = REG_CH1_TX_GAIN + ((channel - 1) & 0x03);
adarWrite(deviceIndex, mem_addr, gain, broadcast);
adarWrite(deviceIndex, REG_LOAD_WORKING, LD_WRK_REGS_LDTX_OVERRIDE, broadcast);
}
@@ -116,10 +116,12 @@ public:
bool beam_sweeping_active_ = false;
uint32_t last_beam_update_time_ = 0;
// Lookup tables
static const uint8_t VM_I[128];
// Vector Modulator lookup tables (see ADAR1000_Manager.cpp for provenance).
// Indexed as VM_*[phase % 128] on a uniform 2.8125 deg grid.
// No VM_GAIN[] table exists: VM magnitude is bits [4:0] of the I/Q bytes
// themselves; per-channel VGA gain uses a separate register.
static const uint8_t VM_I[128];
static const uint8_t VM_Q[128];
static const uint8_t VM_GAIN[128];
// Named defaults for the ADTR1107 and ADAR1000 power sequence.
static constexpr uint8_t kDefaultTxVgaGain = 0x7F;
@@ -1,693 +0,0 @@
/**
* MIT License
*
* Copyright (c) 2020 Jimmy Pentz
*
* Reach me at: github.com/jgpentz, jpentz1(at)gmail.com
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sells
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/* ADAR1000 4-Channel, X Band and Ku Band Beamformer */
// ----------------------------------------------------------------------------
// Includes
// ----------------------------------------------------------------------------
#include "main.h"
#include "stm32f7xx_hal.h"
#include "stm32f7xx_hal_spi.h"
#include "stm32f7xx_hal_gpio.h"
#include "adar1000.h"
// ----------------------------------------------------------------------------
// Preprocessor Definitions and Constants
// ----------------------------------------------------------------------------
// VM_GAIN is 15 dB of gain in 128 steps. ~0.12 dB per step.
// A 15 dB attenuator can be applied on top of these values.
const uint8_t VM_GAIN[128] = {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f,
0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f,
};
// VM_I and VM_Q are the settings for the vector modulator. 128 steps in 360 degrees. ~2.813 degrees per step.
const uint8_t VM_I[128] = {
0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3E, 0x3E, 0x3D, 0x3D, 0x3C, 0x3C, 0x3B, 0x3A, 0x39, 0x38, 0x37,
0x36, 0x35, 0x34, 0x33, 0x32, 0x30, 0x2F, 0x2E, 0x2C, 0x2B, 0x2A, 0x28, 0x27, 0x25, 0x24, 0x22,
0x21, 0x01, 0x03, 0x04, 0x06, 0x07, 0x08, 0x0A, 0x0B, 0x0D, 0x0E, 0x0F, 0x11, 0x12, 0x13, 0x14,
0x16, 0x17, 0x18, 0x19, 0x19, 0x1A, 0x1B, 0x1C, 0x1C, 0x1D, 0x1E, 0x1E, 0x1E, 0x1F, 0x1F, 0x1F,
0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1E, 0x1E, 0x1D, 0x1D, 0x1C, 0x1C, 0x1B, 0x1A, 0x19, 0x18, 0x17,
0x16, 0x15, 0x14, 0x13, 0x12, 0x10, 0x0F, 0x0E, 0x0C, 0x0B, 0x0A, 0x08, 0x07, 0x05, 0x04, 0x02,
0x01, 0x21, 0x23, 0x24, 0x26, 0x27, 0x28, 0x2A, 0x2B, 0x2D, 0x2E, 0x2F, 0x31, 0x32, 0x33, 0x34,
0x36, 0x37, 0x38, 0x39, 0x39, 0x3A, 0x3B, 0x3C, 0x3C, 0x3D, 0x3E, 0x3E, 0x3E, 0x3F, 0x3F, 0x3F,
};
const uint8_t VM_Q[128] = {
0x20, 0x21, 0x23, 0x24, 0x26, 0x27, 0x28, 0x2A, 0x2B, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x33, 0x34,
0x35, 0x36, 0x37, 0x38, 0x38, 0x39, 0x3A, 0x3A, 0x3B, 0x3C, 0x3C, 0x3C, 0x3D, 0x3D, 0x3D, 0x3D,
0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3C, 0x3C, 0x3C, 0x3B, 0x3A, 0x3A, 0x39, 0x38, 0x38, 0x37, 0x36,
0x35, 0x34, 0x33, 0x31, 0x30, 0x2F, 0x2E, 0x2D, 0x2B, 0x2A, 0x28, 0x27, 0x26, 0x24, 0x23, 0x21,
0x20, 0x01, 0x03, 0x04, 0x06, 0x07, 0x08, 0x0A, 0x0B, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x13, 0x14,
0x15, 0x16, 0x17, 0x18, 0x18, 0x19, 0x1A, 0x1A, 0x1B, 0x1C, 0x1C, 0x1C, 0x1D, 0x1D, 0x1D, 0x1D,
0x1D, 0x1D, 0x1D, 0x1D, 0x1D, 0x1C, 0x1C, 0x1C, 0x1B, 0x1A, 0x1A, 0x19, 0x18, 0x18, 0x17, 0x16,
0x15, 0x14, 0x13, 0x11, 0x10, 0x0F, 0x0E, 0x0D, 0x0B, 0x0A, 0x08, 0x07, 0x06, 0x04, 0x03, 0x01,
};
// ----------------------------------------------------------------------------
// Function Definitions
// ----------------------------------------------------------------------------
/**
* @brief Initialize the ADC on the ADAR by setting the ADC with a 2 MHz clk,
* and then enable it.
*
* @param p_adar[in] Adar pointer Which specifies the device and what function
* to use for SPI transfer.
* @param broadcast Send the message as a broadcast to all ADARs in the SPI chain
* if this set to BROADCAST_ON.
*
* @warning This is setup to only read temperature sensor data, not the power detectors.
*/
void Adar_AdcInit(const AdarDevice * p_adar, uint8_t broadcast)
{
uint8_t data;
data = ADAR1000_ADC_2MHZ_CLK | ADAR1000_ADC_EN;
Adar_Write(p_adar, REG_ADC_CONTROL, data, broadcast);
}
/**
* @brief Read a byte of data from the ADAR.
*
* @param p_adar[in] Adar pointer Which specifies the device and what function
* to use for SPI transfer.
* @param broadcast Send the message as a broadcast to all ADARs in the SPI chain
* if this set to BROADCAST_ON.
*
* @return Returns a byte of data that has been converted from the temperature sensor.
*
* @warning This is setup to only read temperature sensor data, not the power detectors.
*/
uint8_t Adar_AdcRead(const AdarDevice * p_adar, uint8_t broadcast)
{
uint8_t data;
// Start the ADC conversion
Adar_Write(p_adar, REG_ADC_CONTROL, ADAR1000_ADC_ST_CONV, broadcast);
// This is blocking for now... wait until data is converted, then read it
while (!(Adar_Read(p_adar, REG_ADC_CONTROL) & 0x01))
{
}
data = Adar_Read(p_adar, REG_ADC_OUT);
return(data);
}
/**
* @brief Requests the device info from a specific ADAR and stores it in the
* provided AdarDeviceInfo struct.
*
* @param p_adar[in] Adar pointer Which specifies the device and what function
* to use for SPI transfer.
* @param info[out] Struct that contains the device info fields.
*
* @return Returns ADAR_ERROR_NOERROR if information was successfully received and stored in the struct.
*/
uint8_t Adar_GetDeviceInfo(const AdarDevice * p_adar, AdarDeviceInfo * info)
{
*((uint8_t *)info) = Adar_Read(p_adar, 0x002);
info->chip_type = Adar_Read(p_adar, 0x003);
info->product_id = ((uint16_t)Adar_Read(p_adar, 0x004)) << 8;
info->product_id |= ((uint16_t)Adar_Read(p_adar, 0x005)) & 0x00ff;
info->scratchpad = Adar_Read(p_adar, 0x00A);
info->spi_rev = Adar_Read(p_adar, 0x00B);
info->vendor_id = ((uint16_t)Adar_Read(p_adar, 0x00C)) << 8;
info->vendor_id |= ((uint16_t)Adar_Read(p_adar, 0x00D)) & 0x00ff;
info->rev_id = Adar_Read(p_adar, 0x045);
return(ADAR_ERROR_NOERROR);
}
/**
* @brief Read the data that is stored in a single ADAR register.
*
* @param p_adar[in] Adar pointer Which specifies the device and what function
* to use for SPI transfer.
* @param mem_addr Memory address of the register you wish to read from.
*
* @return Returns the byte of data that is stored in the desired register.
*
* @warning This function will clear ADDR_ASCN bits.
* @warning The ADAR does not allow for block reads.
*/
uint8_t Adar_Read(const AdarDevice * p_adar, uint32_t mem_addr)
{
uint8_t instruction[3];
// Set SDO active
Adar_Write(p_adar, REG_INTERFACE_CONFIG_A, INTERFACE_CONFIG_A_SDO_ACTIVE, 0);
instruction[0] = 0x80 | ((p_adar->dev_addr & 0x03) << 5);
instruction[0] |= ((0xff00 & mem_addr) >> 8);
instruction[1] = (0xff & mem_addr);
instruction[2] = 0x00;
p_adar->Transfer(instruction, p_adar->p_rx_buffer, ADAR1000_RD_SIZE);
// Set SDO Inactive
Adar_Write(p_adar, REG_INTERFACE_CONFIG_A, 0, 0);
return(p_adar->p_rx_buffer[2]);
}
/**
* @brief Block memory write to an ADAR device.
*
* @pre ADDR_ASCN bits in register zero must be set!
*
* @param p_adar[in] Adar pointer Which specifies the device and what function
* to use for SPI transfer.
* @param mem_addr Memory address of the register you wish to read from.
* @param p_data Pointer to block of data to transfer (must have two unused bytes preceding the data for instruction).
* @param size Size of data in bytes, including the two additional leading bytes.
*
* @warning First two bytes of data will be corrupted if you do not provide two unused leading bytes!
*/
void Adar_ReadBlock(const AdarDevice * p_adar, uint16_t mem_addr, uint8_t * p_data, uint32_t size)
{
// Set SDO active
Adar_Write(p_adar, REG_INTERFACE_CONFIG_A, INTERFACE_CONFIG_A_SDO_ACTIVE | INTERFACE_CONFIG_A_ADDR_ASCN, 0);
// Prepare command
p_data[0] = 0x80 | ((p_adar->dev_addr & 0x03) << 5);
p_data[0] |= ((mem_addr) >> 8) & 0x1F;
p_data[1] = (0xFF & mem_addr);
// Start the transfer
p_adar->Transfer(p_data, p_data, size);
Adar_Write(p_adar, REG_INTERFACE_CONFIG_A, 0, 0);
// Return nothing since we assume this is non-blocking and won't wait around
}
/**
* @brief Sets the Rx/Tx bias currents for the LNA, VM, and VGA to be in either
* low power setting or nominal setting.
*
* @param p_adar[in] Adar pointer Which specifies the device and what function
* to use for SPI transfer.
* @param p_bias[in] An AdarBiasCurrents struct filled with bias settings
* as seen in the datasheet Table 6. SPI Settings for
* Different Power Modules
* @param broadcast Send the message as a broadcast to all ADARs in the SPI chain
* if this set to BROADCAST_ON.
*
* @return Returns ADAR_ERR_NOERROR if the bias currents were set
*/
uint8_t Adar_SetBiasCurrents(const AdarDevice * p_adar, AdarBiasCurrents * p_bias, uint8_t broadcast)
{
uint8_t bias = 0;
// RX LNA/VGA/VM bias
bias = (p_bias->rx_lna & 0x0f);
Adar_Write(p_adar, REG_BIAS_CURRENT_RX_LNA, bias, broadcast); // RX LNA bias
bias = (p_bias->rx_vga & 0x07 << 3) | (p_bias->rx_vm & 0x07);
Adar_Write(p_adar, REG_BIAS_CURRENT_RX, bias, broadcast); // RX VM/VGA bias
// TX VGA/VM/DRV bias
bias = (p_bias->tx_vga & 0x07 << 3) | (p_bias->tx_vm & 0x07);
Adar_Write(p_adar, REG_BIAS_CURRENT_TX, bias, broadcast); // TX VM/VGA bias
bias = (p_bias->tx_drv & 0x07);
Adar_Write(p_adar, REG_BIAS_CURRENT_TX_DRV, bias, broadcast); // TX DRV bias
return(ADAR_ERROR_NOERROR);
}
/**
* @brief Set the bias ON and bias OFF voltages for the four PA's and one LNA.
*
* @pre This will set all 5 bias ON values and all 5 bias OFF values at once.
* To enable these bias values, please see the data sheet and ensure that the BIAS_CTRL,
* LNA_BIAS_OUT_EN, TR_SOURCE, TX_EN, RX_EN, TR (input to chip), and PA_ON (input to chip)
* bits have all been properly set.
*
* @param p_adar[in] Adar pointer Which specifies the device and what function
* to use for SPI transfer.
* @param bias_on_voltage Array that contains the bias ON voltages.
* @param bias_off_voltage Array that contains the bias OFF voltages.
*
* @return Returns ADAR_ERR_NOERROR if the bias currents were set
*/
uint8_t Adar_SetBiasVoltages(const AdarDevice * p_adar, uint8_t bias_on_voltage[5], uint8_t bias_off_voltage[5])
{
Adar_SetBit(p_adar, 0x30, 6, BROADCAST_OFF);
Adar_SetBit(p_adar, 0x31, 2, BROADCAST_OFF);
Adar_SetBit(p_adar, 0x38, 5, BROADCAST_OFF);
Adar_Write(p_adar, REG_PA_CH1_BIAS_ON,bias_on_voltage[0], BROADCAST_OFF);
Adar_Write(p_adar, REG_PA_CH2_BIAS_ON,bias_on_voltage[1], BROADCAST_OFF);
Adar_Write(p_adar, REG_PA_CH3_BIAS_ON,bias_on_voltage[2], BROADCAST_OFF);
Adar_Write(p_adar, REG_PA_CH4_BIAS_ON,bias_on_voltage[3], BROADCAST_OFF);
Adar_Write(p_adar, REG_PA_CH1_BIAS_OFF,bias_off_voltage[0], BROADCAST_OFF);
Adar_Write(p_adar, REG_PA_CH2_BIAS_OFF,bias_off_voltage[1], BROADCAST_OFF);
Adar_Write(p_adar, REG_PA_CH3_BIAS_OFF,bias_off_voltage[2], BROADCAST_OFF);
Adar_Write(p_adar, REG_PA_CH4_BIAS_OFF,bias_off_voltage[3], BROADCAST_OFF);
Adar_SetBit(p_adar, 0x30, 4, BROADCAST_OFF);
Adar_SetBit(p_adar, 0x30, 6, BROADCAST_OFF);
Adar_SetBit(p_adar, 0x31, 2, BROADCAST_OFF);
Adar_SetBit(p_adar, 0x38, 5, BROADCAST_OFF);
Adar_Write(p_adar, REG_LNA_BIAS_ON,bias_on_voltage[4], BROADCAST_OFF);
Adar_Write(p_adar, REG_LNA_BIAS_OFF,bias_off_voltage[4], BROADCAST_OFF);
Adar_ResetBit(p_adar, 0x30, 7, BROADCAST_OFF);
Adar_SetBit(p_adar, 0x31, 2, BROADCAST_OFF);
Adar_SetBit(p_adar, 0x31, 4, BROADCAST_OFF);
Adar_SetBit(p_adar, 0x31, 7, BROADCAST_OFF);
return(ADAR_ERROR_NOERROR);
}
/**
* @brief Setup the ADAR to use settings that are transferred over SPI.
*
* @param p_adar[in] Adar pointer Which specifies the device and what function
* to use for SPI transfer.
* @param broadcast Send the message as a broadcast to all ADARs in the SPI chain
* if this set to BROADCAST_ON.
*
* @return Returns ADAR_ERR_NOERROR if the bias currents were set
*/
uint8_t Adar_SetRamBypass(const AdarDevice * p_adar, uint8_t broadcast)
{
uint8_t data;
data = (MEM_CTRL_BIAS_RAM_BYPASS | MEM_CTRL_BEAM_RAM_BYPASS);
Adar_Write(p_adar, REG_MEM_CTL, data, broadcast);
return(ADAR_ERROR_NOERROR);
}
/**
* @brief Set the VGA gain value of a Receive channel in dB.
*
* @param p_adar[in] Adar pointer Which specifies the device and what function
* to use for SPI transfer.
* @param channel Channel in which to set the gain (1-4).
* @param vga_gain_db Gain to be applied to the channel, ranging from 0 - 30 dB.
* (Intended operation >16 dB).
* @param broadcast Send the message as a broadcast to all ADARs in the SPI chain
* if this set to BROADCAST_ON.
*
* @return Returns ADAR_ERROR_NOERROR if the gain was successfully set.
* ADAR_ERROR_FAILED if an invalid channel was selected.
*
* @warning 0 dB or 15 dB step attenuator may also be turned on, which is why intended operation is >16 dB.
*/
uint8_t Adar_SetRxVgaGain(const AdarDevice * p_adar, uint8_t channel, uint8_t vga_gain_db, uint8_t broadcast)
{
uint8_t vga_gain_bits = (uint8_t)(255*vga_gain_db/16);
uint32_t mem_addr = 0;
if((channel == 0) || (channel > 4))
{
return(ADAR_ERROR_FAILED);
}
mem_addr = REG_CH1_RX_GAIN + (channel & 0x03);
// Set gain
Adar_Write(p_adar, mem_addr, vga_gain_bits, broadcast);
// Load the new setting
Adar_Write(p_adar, REG_LOAD_WORKING, 0x1, broadcast);
return(ADAR_ERROR_NOERROR);
}
/**
* @brief Set the phase of a given receive channel using the I/Q vector modulator.
*
* @pre According to the given @param phase, this sets the polarity (bit 5) and gain (bits 4-0)
* of the @param channel, and then loads them into the working register.
* A vector modulator I/Q look-up table has been provided at the beginning of this library.
*
* @param p_adar[in] Adar pointer Which specifies the device and what function
* to use for SPI transfer.
* @param channel Channel in which to set the gain (1-4).
* @param phase Byte that is used to set the polarity (bit 5) and gain (bits 4-0).
* @param broadcast Send the message as a broadcast to all ADARs in the SPI chain
* if this set to BROADCAST_ON.
*
* @return Returns ADAR_ERROR_NOERROR if the phase was successfully set.
* ADAR_ERROR_FAILED if an invalid channel was selected.
*
* @note To obtain your phase:
* phase = degrees * 128;
* phase /= 360;
*/
uint8_t Adar_SetRxPhase(const AdarDevice * p_adar, uint8_t channel, uint8_t phase, uint8_t broadcast)
{
uint8_t i_val = 0;
uint8_t q_val = 0;
uint32_t mem_addr_i, mem_addr_q;
if((channel == 0) || (channel > 4))
{
return(ADAR_ERROR_FAILED);
}
//phase = phase % 128;
i_val = VM_I[phase];
q_val = VM_Q[phase];
mem_addr_i = REG_CH1_RX_PHS_I + (channel & 0x03) * 2;
mem_addr_q = REG_CH1_RX_PHS_Q + (channel & 0x03) * 2;
Adar_Write(p_adar, mem_addr_i, i_val, broadcast);
Adar_Write(p_adar, mem_addr_q, q_val, broadcast);
Adar_Write(p_adar, REG_LOAD_WORKING, 0x1, broadcast);
return(ADAR_ERROR_NOERROR);
}
/**
* @brief Set the VGA gain value of a Tx channel in dB.
*
* @param p_adar[in] Adar pointer Which specifies the device and what function
* to use for SPI transfer.
* @param broadcast Send the message as a broadcast to all ADARs in the SPI chain
* if this set to BROADCAST_ON.
*
* @return Returns ADAR_ERROR_NOERROR if the bias was successfully set.
* ADAR_ERROR_FAILED if an invalid channel was selected.
*
* @warning 0 dB or 15 dB step attenuator may also be turned on, which is why intended operation is >16 dB.
*/
uint8_t Adar_SetTxBias(const AdarDevice * p_adar, uint8_t broadcast)
{
uint8_t vga_bias_bits;
uint8_t drv_bias_bits;
uint32_t mem_vga_bias;
uint32_t mem_drv_bias;
mem_vga_bias = REG_BIAS_CURRENT_TX;
mem_drv_bias = REG_BIAS_CURRENT_TX_DRV;
// Set bias to nom
vga_bias_bits = 0x2D;
drv_bias_bits = 0x06;
// Set bias
Adar_Write(p_adar, mem_vga_bias, vga_bias_bits, broadcast);
// Set bias
Adar_Write(p_adar, mem_drv_bias, drv_bias_bits, broadcast);
// Load the new setting
Adar_Write(p_adar, REG_LOAD_WORKING, 0x2, broadcast);
return(ADAR_ERROR_NOERROR);
}
/**
* @brief Set the VGA gain value of a Tx channel.
*
* @param p_adar[in] Adar pointer Which specifies the device and what function
* to use for SPI transfer.
* @param channel Tx channel in which to set the gain, ranging from 1 - 4.
* @param gain Gain to be applied to the channel, ranging from 0 - 127,
* plus the MSb 15dB attenuator (Intended operation >16 dB).
* @param broadcast Send the message as a broadcast to all ADARs in the SPI chain
* if this set to BROADCAST_ON.
*
* @return Returns ADAR_ERROR_NOERROR if the gain was successfully set.
* ADAR_ERROR_FAILED if an invalid channel was selected.
*
* @warning 0 dB or 15 dB step attenuator may also be turned on, which is why intended operation is >16 dB.
*/
uint8_t Adar_SetTxVgaGain(const AdarDevice * p_adar, uint8_t channel, uint8_t gain, uint8_t broadcast)
{
uint32_t mem_addr;
if((channel == 0) || (channel > 4))
{
return(ADAR_ERROR_FAILED);
}
mem_addr = REG_CH1_TX_GAIN + (channel & 0x03);
// Set gain
Adar_Write(p_adar, mem_addr, gain, broadcast);
// Load the new setting
Adar_Write(p_adar, REG_LOAD_WORKING, LD_WRK_REGS_LDTX_OVERRIDE, broadcast);
return(ADAR_ERROR_NOERROR);
}
/**
* @brief Set the phase of a given transmit channel using the I/Q vector modulator.
*
* @pre According to the given @param phase, this sets the polarity (bit 5) and gain (bits 4-0)
* of the @param channel, and then loads them into the working register.
* A vector modulator I/Q look-up table has been provided at the beginning of this library.
*
* @param p_adar[in] Adar pointer Which specifies the device and what function
* to use for SPI transfer.
* @param channel Channel in which to set the gain (1-4).
* @param phase Byte that is used to set the polarity (bit 5) and gain (bits 4-0).
* @param broadcast Send the message as a broadcast to all ADARs in the SPI chain
* if this set to BROADCAST_ON.
*
* @return Returns ADAR_ERROR_NOERROR if the phase was successfully set.
* ADAR_ERROR_FAILED if an invalid channel was selected.
*
* @note To obtain your phase:
* phase = degrees * 128;
* phase /= 360;
*/
uint8_t Adar_SetTxPhase(const AdarDevice * p_adar, uint8_t channel, uint8_t phase, uint8_t broadcast)
{
uint8_t i_val = 0;
uint8_t q_val = 0;
uint32_t mem_addr_i, mem_addr_q;
if((channel == 0) || (channel > 4))
{
return(ADAR_ERROR_FAILED);
}
//phase = phase % 128;
i_val = VM_I[phase];
q_val = VM_Q[phase];
mem_addr_i = REG_CH1_TX_PHS_I + (channel & 0x03) * 2;
mem_addr_q = REG_CH1_TX_PHS_Q + (channel & 0x03) * 2;
Adar_Write(p_adar, mem_addr_i, i_val, broadcast);
Adar_Write(p_adar, mem_addr_q, q_val, broadcast);
Adar_Write(p_adar, REG_LOAD_WORKING, 0x1, broadcast);
return(ADAR_ERROR_NOERROR);
}
/**
* @brief Reset the whole ADAR device.
*
* @param p_adar[in] ADAR pointer Which specifies the device and what function
* to use for SPI transfer.
*/
void Adar_SoftReset(const AdarDevice * p_adar)
{
uint8_t instruction[3];
instruction[0] = ((p_adar->dev_addr & 0x03) << 5);
instruction[1] = 0x00;
instruction[2] = 0x81;
p_adar->Transfer(instruction, NULL, sizeof(instruction));
}
/**
* @brief Reset ALL ADAR devices in the SPI chain.
*
* @param p_adar[in] Adar pointer Which specifies the device and what function
* to use for SPI transfer.
*/
void Adar_SoftResetAll(const AdarDevice * p_adar)
{
uint8_t instruction[3];
instruction[0] = 0x08;
instruction[1] = 0x00;
instruction[2] = 0x81;
p_adar->Transfer(instruction, NULL, sizeof(instruction));
}
/**
* @brief Write a byte of @param data to the register located at @param mem_addr.
*
* @param p_adar[in] Adar pointer Which specifies the device and what function
* to use for SPI transfer.
* @param mem_addr Memory address of the register you wish to read from.
* @param data Byte of data to be stored in the register.
* @param broadcast Send the message as a broadcast to all ADARs in the SPI chain
if this set to BROADCAST_ON.
*
* @warning If writing the same data to multiple registers, use ADAR_WriteBlock.
*/
void Adar_Write(const AdarDevice * p_adar, uint32_t mem_addr, uint8_t data, uint8_t broadcast)
{
uint8_t instruction[3];
if (broadcast)
{
instruction[0] = 0x08;
}
else
{
instruction[0] = ((p_adar->dev_addr & 0x03) << 5);
}
instruction[0] |= (0x1F00 & mem_addr) >> 8;
instruction[1] = (0xFF & mem_addr);
instruction[2] = data;
p_adar->Transfer(instruction, NULL, sizeof(instruction));
}
/**
* @brief Block memory write to an ADAR device.
*
* @pre ADDR_ASCN BITS IN REGISTER ZERO MUST BE SET!
*
* @param p_adar[in] Adar pointer Which specifies the device and what function
* to use for SPI transfer.
* @param mem_addr Memory address of the register you wish to read from.
* @param p_data[in] Pointer to block of data to transfer (must have two unused bytes
preceding the data for instruction).
* @param size Size of data in bytes, including the two additional leading bytes.
*
* @warning First two bytes of data will be corrupted if you do not provide two unused leading bytes!
*/
void Adar_WriteBlock(const AdarDevice * p_adar, uint16_t mem_addr, uint8_t * p_data, uint32_t size)
{
// Prepare command
p_data[0] = ((p_adar->dev_addr & 0x03) << 5);
p_data[0] |= ((mem_addr) >> 8) & 0x1F;
p_data[1] = (0xFF & mem_addr);
// Start the transfer
p_adar->Transfer(p_data, NULL, size);
// Return nothing since we assume this is non-blocking and won't wait around
}
/**
* @brief Set contents of the INTERFACE_CONFIG_A register.
*
* @param p_adar[in] Adar pointer Which specifies the device and what function
* to use for SPI transfer.
* @param flags #INTERFACE_CONFIG_A_SOFTRESET, #INTERFACE_CONFIG_A_LSB_FIRST,
* #INTERFACE_CONFIG_A_ADDR_ASCN, #INTERFACE_CONFIG_A_SDO_ACTIVE
* @param broadcast Send the message as a broadcast to all ADARs in the SPI chain
* if this set to BROADCAST_ON.
*/
void Adar_WriteConfigA(const AdarDevice * p_adar, uint8_t flags, uint8_t broadcast)
{
Adar_Write(p_adar, 0x00, flags, broadcast);
}
/**
* @brief Write a byte of @param data to the register located at @param mem_addr and
* then read from the device and verify that the register was correctly set.
*
* @param p_adar[in] Adar pointer Which specifies the device and what function
* to use for SPI transfer.
* @param mem_addr Memory address of the register you wish to read from.
* @param data Byte of data to be stored in the register.
*
* @return Returns the number of attempts that it took to successfully write to a register,
* starting from zero.
* @warning This function currently only supports writes to a single regiter in a single ADAR.
*/
uint8_t Adar_WriteVerify(const AdarDevice * p_adar, uint32_t mem_addr, uint8_t data)
{
uint8_t rx_data;
for (uint8_t ii = 0; ii < 3; ii++)
{
Adar_Write(p_adar, mem_addr, data, 0);
// Can't read back from an ADAR with HW address 0
if (!((p_adar->dev_addr) % 4))
{
return(ADAR_ERROR_INVALIDADDR);
}
rx_data = Adar_Read(p_adar, mem_addr);
if (rx_data == data)
{
return(ii);
}
}
return(ADAR_ERROR_FAILED);
}
void Adar_SetBit(const AdarDevice * p_adar, uint32_t mem_addr, uint8_t bit, uint8_t broadcast)
{
uint8_t temp = Adar_Read(p_adar, mem_addr);
uint8_t data = temp|(1<<bit);
Adar_Write(p_adar,mem_addr, data,broadcast);
}
void Adar_ResetBit(const AdarDevice * p_adar, uint32_t mem_addr, uint8_t bit, uint8_t broadcast)
{
uint8_t temp = Adar_Read(p_adar, mem_addr);
uint8_t data = temp&~(1<<bit);
Adar_Write(p_adar,mem_addr, data,broadcast);
}
@@ -1,294 +0,0 @@
/**
* MIT License
*
* Copyright (c) 2020 Jimmy Pentz
*
* Reach me at: github.com/jgpentz, jpentz1( at )gmail.com
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sells
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/* ADAR1000 4-Channel, X Band and Ku Band Beamformer */
#ifndef LIB_ADAR1000_H_
#define LIB_ADAR1000_H_
#ifndef NULL
#define NULL (0)
#endif
// ----------------------------------------------------------------------------
// Includes
// ----------------------------------------------------------------------------
#include "main.h"
#include "stm32f7xx_hal.h"
#include "stm32f7xx_hal_spi.h"
#include "stm32f7xx_hal_gpio.h"
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#ifdef __cplusplus
extern "C" { // Prevent C++ name mangling
#endif
// ----------------------------------------------------------------------------
// Datatypes
// ----------------------------------------------------------------------------
extern SPI_HandleTypeDef hspi1;
extern const uint8_t VM_GAIN[128];
extern const uint8_t VM_I[128];
extern const uint8_t VM_Q[128];
/// A function pointer prototype for a SPI transfer, the 3 parameters would be
/// p_txData, p_rxData, and size (number of bytes to transfer), respectively.
typedef uint32_t (*Adar_SpiTransfer)( uint8_t *, uint8_t *, uint32_t);
typedef struct
{
uint8_t dev_addr; ///< 2-bit device hardware address, 0x00, 0x01, 0x10, 0x11
Adar_SpiTransfer Transfer; ///< Function pointer to the function used for SPI transfers
uint8_t * p_rx_buffer; ///< Data buffer to store received bytes into
}const AdarDevice;
/// Use this to store bias current values into, as seen in the datasheet
/// Table 6. SPI Settings for Different Power Modules
typedef struct
{
uint8_t rx_lna; ///< nominal: 8, low power: 5
uint8_t rx_vm; ///< nominal: 5, low power: 2
uint8_t rx_vga; ///< nominal: 10, low power: 3
uint8_t tx_vm; ///< nominal: 5, low power: 2
uint8_t tx_vga; ///< nominal: 5, low power: 5
uint8_t tx_drv; ///< nominal: 6, low power: 3
} AdarBiasCurrents;
/// Useful for queries regarding the device info
typedef struct
{
uint8_t norm_operating_mode : 2;
uint8_t cust_operating_mode : 2;
uint8_t dev_status : 4;
uint8_t chip_type;
uint16_t product_id;
uint8_t scratchpad;
uint8_t spi_rev;
uint16_t vendor_id;
uint8_t rev_id;
} AdarDeviceInfo;
/// Return types for functions in this library
typedef enum {
ADAR_ERROR_NOERROR = 0,
ADAR_ERROR_FAILED = 1,
ADAR_ERROR_INVALIDADDR = 2,
} AdarErrorCodes;
// ----------------------------------------------------------------------------
// Function Prototypes
// ----------------------------------------------------------------------------
void Adar_AdcInit(const AdarDevice * p_adar, uint8_t broadcast_bit);
uint8_t Adar_AdcRead(const AdarDevice * p_adar, uint8_t broadcast_bit);
uint8_t Adar_GetDeviceInfo(const AdarDevice * p_adar, AdarDeviceInfo * info);
uint8_t Adar_Read(const AdarDevice * p_adar, uint32_t mem_addr);
void Adar_ReadBlock(const AdarDevice * p_adar, uint16_t mem_addr, uint8_t * p_data, uint32_t size);
uint8_t Adar_SetBiasCurrents(const AdarDevice * p_adar, AdarBiasCurrents * p_bias, uint8_t broadcast_bit);
uint8_t Adar_SetBiasVoltages(const AdarDevice * p_adar, uint8_t bias_on_voltage[5], uint8_t bias_off_voltage[5]);
uint8_t Adar_SetRamBypass(const AdarDevice * p_adar, uint8_t broadcast_bit);
uint8_t Adar_SetRxVgaGain(const AdarDevice * p_adar, uint8_t channel, uint8_t vga_gain_db, uint8_t broadcast_bit);
uint8_t Adar_SetRxPhase(const AdarDevice * p_adar, uint8_t channel, uint8_t phase, uint8_t broadcast_bit);
uint8_t Adar_SetTxBias(const AdarDevice * p_adar, uint8_t broadcast_bit);
uint8_t Adar_SetTxVgaGain(const AdarDevice * p_adar, uint8_t channel, uint8_t vga_gain_db, uint8_t broadcast_bit);
uint8_t Adar_SetTxPhase(const AdarDevice * p_adar, uint8_t channel, uint8_t phase, uint8_t broadcast_bit);
void Adar_SoftReset(const AdarDevice * p_adar);
void Adar_SoftResetAll(const AdarDevice * p_adar);
void Adar_Write(const AdarDevice * p_adar, uint32_t mem_addr, uint8_t data, uint8_t broadcast_bit);
void Adar_WriteBlock(const AdarDevice * p_adar, uint16_t mem_addr, uint8_t * p_data, uint32_t size);
void Adar_WriteConfigA(const AdarDevice * p_adar, uint8_t flags, uint8_t broadcast);
uint8_t Adar_WriteVerify(const AdarDevice * p_adar, uint32_t mem_addr, uint8_t data);
void Adar_SetBit(const AdarDevice * p_adar, uint32_t mem_addr, uint8_t bit, uint8_t broadcast);
void Adar_ResetBit(const AdarDevice * p_adar, uint32_t mem_addr, uint8_t bit, uint8_t broadcast);
// ----------------------------------------------------------------------------
// Preprocessor Definitions and Constants
// ----------------------------------------------------------------------------
// Using BROADCAST_ON will send a command to all ADARs that share a bus
#define BROADCAST_OFF 0
#define BROADCAST_ON 1
// The minimum size of a read from the ADARs consists of 3 bytes
#define ADAR1000_RD_SIZE 3
// Address at which the TX RAM starts
#define ADAR_TX_RAM_START_ADDR 0x1800
// ADC Defines
#define ADAR1000_ADC_2MHZ_CLK 0x00
#define ADAR1000_ADC_EN 0x60
#define ADAR1000_ADC_ST_CONV 0x70
/* REGISTER DEFINITIONS */
#define REG_INTERFACE_CONFIG_A 0x000
#define REG_INTERFACE_CONFIG_B 0x001
#define REG_DEV_CONFIG 0x002
#define REG_SCRATCHPAD 0x00A
#define REG_TRANSFER 0x00F
#define REG_CH1_RX_GAIN 0x010
#define REG_CH2_RX_GAIN 0x011
#define REG_CH3_RX_GAIN 0x012
#define REG_CH4_RX_GAIN 0x013
#define REG_CH1_RX_PHS_I 0x014
#define REG_CH1_RX_PHS_Q 0x015
#define REG_CH2_RX_PHS_I 0x016
#define REG_CH2_RX_PHS_Q 0x017
#define REG_CH3_RX_PHS_I 0x018
#define REG_CH3_RX_PHS_Q 0x019
#define REG_CH4_RX_PHS_I 0x01A
#define REG_CH4_RX_PHS_Q 0x01B
#define REG_CH1_TX_GAIN 0x01C
#define REG_CH2_TX_GAIN 0x01D
#define REG_CH3_TX_GAIN 0x01E
#define REG_CH4_TX_GAIN 0x01F
#define REG_CH1_TX_PHS_I 0x020
#define REG_CH1_TX_PHS_Q 0x021
#define REG_CH2_TX_PHS_I 0x022
#define REG_CH2_TX_PHS_Q 0x023
#define REG_CH3_TX_PHS_I 0x024
#define REG_CH3_TX_PHS_Q 0x025
#define REG_CH4_TX_PHS_I 0x026
#define REG_CH4_TX_PHS_Q 0x027
#define REG_LOAD_WORKING 0x028
#define REG_PA_CH1_BIAS_ON 0x029
#define REG_PA_CH2_BIAS_ON 0x02A
#define REG_PA_CH3_BIAS_ON 0x02B
#define REG_PA_CH4_BIAS_ON 0x02C
#define REG_LNA_BIAS_ON 0x02D
#define REG_RX_ENABLES 0x02E
#define REG_TX_ENABLES 0x02F
#define REG_MISC_ENABLES 0x030
#define REG_SW_CONTROL 0x031
#define REG_ADC_CONTROL 0x032
#define REG_ADC_CONTROL_TEMP_EN 0xf0
#define REG_ADC_OUT 0x033
#define REG_BIAS_CURRENT_RX_LNA 0x034
#define REG_BIAS_CURRENT_RX 0x035
#define REG_BIAS_CURRENT_TX 0x036
#define REG_BIAS_CURRENT_TX_DRV 0x037
#define REG_MEM_CTL 0x038
#define REG_RX_CHX_MEM 0x039
#define REG_TX_CHX_MEM 0x03A
#define REG_RX_CH1_MEM 0x03D
#define REG_RX_CH2_MEM 0x03E
#define REG_RX_CH3_MEM 0x03F
#define REG_RX_CH4_MEM 0x040
#define REG_TX_CH1_MEM 0x041
#define REG_TX_CH2_MEM 0x042
#define REG_TX_CH3_MEM 0x043
#define REG_TX_CH4_MEM 0x044
#define REG_PA_CH1_BIAS_OFF 0x046
#define REG_PA_CH2_BIAS_OFF 0x047
#define REG_PA_CH3_BIAS_OFF 0x048
#define REG_PA_CH4_BIAS_OFF 0x049
#define REG_LNA_BIAS_OFF 0x04A
#define REG_TX_BEAM_STEP_START 0x04D
#define REG_TX_BEAM_STEP_STOP 0x04E
#define REG_RX_BEAM_STEP_START 0x04F
#define REG_RX_BEAM_STEP_STOP 0x050
// REGISTER CONSTANTS
#define INTERFACE_CONFIG_A_SOFTRESET ((1 << 7) | (1 << 0))
#define INTERFACE_CONFIG_A_LSB_FIRST ((1 << 6) | (1 << 1))
#define INTERFACE_CONFIG_A_ADDR_ASCN ((1 << 5) | (1 << 2))
#define INTERFACE_CONFIG_A_SDO_ACTIVE ((1 << 4) | (1 << 3))
#define LD_WRK_REGS_LDRX_OVERRIDE (1 << 0)
#define LD_WRK_REGS_LDTX_OVERRIDE (1 << 1)
#define RX_ENABLES_TX_VGA_EN (1 << 0)
#define RX_ENABLES_TX_VM_EN (1 << 1)
#define RX_ENABLES_TX_DRV_EN (1 << 2)
#define RX_ENABLES_CH3_TX_EN (1 << 3)
#define RX_ENABLES_CH2_TX_EN (1 << 4)
#define RX_ENABLES_CH1_TX_EN (1 << 5)
#define RX_ENABLES_CH0_TX_EN (1 << 6)
#define TX_ENABLES_TX_VGA_EN (1 << 0)
#define TX_ENABLES_TX_VM_EN (1 << 1)
#define TX_ENABLES_TX_DRV_EN (1 << 2)
#define TX_ENABLES_CH3_TX_EN (1 << 3)
#define TX_ENABLES_CH2_TX_EN (1 << 4)
#define TX_ENABLES_CH1_TX_EN (1 << 5)
#define TX_ENABLES_CH0_TX_EN (1 << 6)
#define MISC_ENABLES_CH4_DET_EN (1 << 0)
#define MISC_ENABLES_CH3_DET_EN (1 << 1)
#define MISC_ENABLES_CH2_DET_EN (1 << 2)
#define MISC_ENABLES_CH1_DET_EN (1 << 3)
#define MISC_ENABLES_LNA_BIAS_OUT_EN (1 << 4)
#define MISC_ENABLES_BIAS_EN (1 << 5)
#define MISC_ENABLES_BIAS_CTRL (1 << 6)
#define MISC_ENABLES_SW_DRV_TR_MODE_SEL (1 << 7)
#define SW_CTRL_POL (1 << 0)
#define SW_CTRL_TR_SPI (1 << 1)
#define SW_CTRL_TR_SOURCE (1 << 2)
#define SW_CTRL_SW_DRV_EN_POL (1 << 3)
#define SW_CTRL_SW_DRV_EN_TR (1 << 4)
#define SW_CTRL_RX_EN (1 << 5)
#define SW_CTRL_TX_EN (1 << 6)
#define SW_CTRL_SW_DRV_TR_STATE (1 << 7)
#define MEM_CTRL_RX_CHX_RAM_BYPASS (1 << 0)
#define MEM_CTRL_TX_CHX_RAM_BYPASS (1 << 1)
#define MEM_CTRL_RX_BEAM_STEP_EN (1 << 2)
#define MEM_CTRL_TX_BEAM_STEP_EN (1 << 3)
#define MEM_CTRL_BIAS_RAM_BYPASS (1 << 5)
#define MEM_CTRL_BEAM_RAM_BYPASS (1 << 6)
#define MEM_CTRL_SCAN_MODE_EN (1 << 7)
#ifdef __cplusplus
} // End extern "C"
#endif
#endif /* LIB_ADAR1000_H_ */
@@ -21,7 +21,6 @@
#include "usb_device.h"
#include "USBHandler.h"
#include "usbd_cdc_if.h"
#include "adar1000.h"
#include "ADAR1000_Manager.h"
#include "ADAR1000_AGC.h"
extern "C" {
@@ -0,0 +1,216 @@
"""ADAR1000 vector-modulator ground-truth table and firmware parser.
This module is a pure data + helpers library imported by the cross-layer
test suite (`9_Firmware/tests/cross_layer/test_cross_layer_contract.py`,
class `TestTier2Adar1000VmTableGroundTruth`). It has no CLI entry point
and no side effects on import beyond the structural assertion on the
table length.
Ground-truth source
-------------------
The 128-entry `(I, Q)` byte pairs below are transcribed from the ADAR1000
datasheet Rev. B, Tables 13-16, page 34 ("Phase Shifter Programming"),
which is the primary normative reference. The same values appear in the
Analog Devices Linux beamformer driver
(`drivers/iio/beamformer/adar1000.c`, `adar1000_phase_values[]`) and were
cross-checked against that driver as a secondary, independent
transcription. The byte values are factual data (5-bit unsigned magnitude
in bits[4:0], polarity bit at bit[5], bits[7:6] reserved zero); no
copyrightable creative expression. Only the datasheet is the
licensing-relevant source.
PLFM_RADAR firmware indexing convention
---------------------------------------
`adarSetRxPhase` / `adarSetTxPhase` in
`9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/ADAR1000_Manager.cpp`
write `VM_I[phase % 128]` and `VM_Q[phase % 128]` to the chip. Each index
N corresponds to commanded beam phase `N * 360/128 = N * 2.8125 deg`. The
ADI table is also on a uniform 2.8125 deg grid (verified by
`check_uniform_2p8125_deg_step` below), so a 1:1 mapping is correct:
PLFM index N == ADI table row N.
"""
from __future__ import annotations
import re
# ----------------------------------------------------------------------------
# Ground truth: ADAR1000 datasheet Rev. B Tables 13-16 p.34
# Each entry: (angle_int_deg, angle_frac_x10000, vm_byte_I, vm_byte_Q)
# ----------------------------------------------------------------------------
GROUND_TRUTH: list[tuple[int, int, int, int]] = [
(0, 0, 0x3F, 0x20), (2, 8125, 0x3F, 0x21), (5, 6250, 0x3F, 0x23),
(8, 4375, 0x3F, 0x24), (11, 2500, 0x3F, 0x26), (14, 625, 0x3E, 0x27),
(16, 8750, 0x3E, 0x28), (19, 6875, 0x3D, 0x2A), (22, 5000, 0x3D, 0x2B),
(25, 3125, 0x3C, 0x2D), (28, 1250, 0x3C, 0x2E), (30, 9375, 0x3B, 0x2F),
(33, 7500, 0x3A, 0x30), (36, 5625, 0x39, 0x31), (39, 3750, 0x38, 0x33),
(42, 1875, 0x37, 0x34), (45, 0, 0x36, 0x35), (47, 8125, 0x35, 0x36),
(50, 6250, 0x34, 0x37), (53, 4375, 0x33, 0x38), (56, 2500, 0x32, 0x38),
(59, 625, 0x30, 0x39), (61, 8750, 0x2F, 0x3A), (64, 6875, 0x2E, 0x3A),
(67, 5000, 0x2C, 0x3B), (70, 3125, 0x2B, 0x3C), (73, 1250, 0x2A, 0x3C),
(75, 9375, 0x28, 0x3C), (78, 7500, 0x27, 0x3D), (81, 5625, 0x25, 0x3D),
(84, 3750, 0x24, 0x3D), (87, 1875, 0x22, 0x3D), (90, 0, 0x21, 0x3D),
(92, 8125, 0x01, 0x3D), (95, 6250, 0x03, 0x3D), (98, 4375, 0x04, 0x3D),
(101, 2500, 0x06, 0x3D), (104, 625, 0x07, 0x3C), (106, 8750, 0x08, 0x3C),
(109, 6875, 0x0A, 0x3C), (112, 5000, 0x0B, 0x3B), (115, 3125, 0x0D, 0x3A),
(118, 1250, 0x0E, 0x3A), (120, 9375, 0x0F, 0x39), (123, 7500, 0x11, 0x38),
(126, 5625, 0x12, 0x38), (129, 3750, 0x13, 0x37), (132, 1875, 0x14, 0x36),
(135, 0, 0x16, 0x35), (137, 8125, 0x17, 0x34), (140, 6250, 0x18, 0x33),
(143, 4375, 0x19, 0x31), (146, 2500, 0x19, 0x30), (149, 625, 0x1A, 0x2F),
(151, 8750, 0x1B, 0x2E), (154, 6875, 0x1C, 0x2D), (157, 5000, 0x1C, 0x2B),
(160, 3125, 0x1D, 0x2A), (163, 1250, 0x1E, 0x28), (165, 9375, 0x1E, 0x27),
(168, 7500, 0x1E, 0x26), (171, 5625, 0x1F, 0x24), (174, 3750, 0x1F, 0x23),
(177, 1875, 0x1F, 0x21), (180, 0, 0x1F, 0x20), (182, 8125, 0x1F, 0x01),
(185, 6250, 0x1F, 0x03), (188, 4375, 0x1F, 0x04), (191, 2500, 0x1F, 0x06),
(194, 625, 0x1E, 0x07), (196, 8750, 0x1E, 0x08), (199, 6875, 0x1D, 0x0A),
(202, 5000, 0x1D, 0x0B), (205, 3125, 0x1C, 0x0D), (208, 1250, 0x1C, 0x0E),
(210, 9375, 0x1B, 0x0F), (213, 7500, 0x1A, 0x10), (216, 5625, 0x19, 0x11),
(219, 3750, 0x18, 0x13), (222, 1875, 0x17, 0x14), (225, 0, 0x16, 0x15),
(227, 8125, 0x15, 0x16), (230, 6250, 0x14, 0x17), (233, 4375, 0x13, 0x18),
(236, 2500, 0x12, 0x18), (239, 625, 0x10, 0x19), (241, 8750, 0x0F, 0x1A),
(244, 6875, 0x0E, 0x1A), (247, 5000, 0x0C, 0x1B), (250, 3125, 0x0B, 0x1C),
(253, 1250, 0x0A, 0x1C), (255, 9375, 0x08, 0x1C), (258, 7500, 0x07, 0x1D),
(261, 5625, 0x05, 0x1D), (264, 3750, 0x04, 0x1D), (267, 1875, 0x02, 0x1D),
(270, 0, 0x01, 0x1D), (272, 8125, 0x21, 0x1D), (275, 6250, 0x23, 0x1D),
(278, 4375, 0x24, 0x1D), (281, 2500, 0x26, 0x1D), (284, 625, 0x27, 0x1C),
(286, 8750, 0x28, 0x1C), (289, 6875, 0x2A, 0x1C), (292, 5000, 0x2B, 0x1B),
(295, 3125, 0x2D, 0x1A), (298, 1250, 0x2E, 0x1A), (300, 9375, 0x2F, 0x19),
(303, 7500, 0x31, 0x18), (306, 5625, 0x32, 0x18), (309, 3750, 0x33, 0x17),
(312, 1875, 0x34, 0x16), (315, 0, 0x36, 0x15), (317, 8125, 0x37, 0x14),
(320, 6250, 0x38, 0x13), (323, 4375, 0x39, 0x11), (326, 2500, 0x39, 0x10),
(329, 625, 0x3A, 0x0F), (331, 8750, 0x3B, 0x0E), (334, 6875, 0x3C, 0x0D),
(337, 5000, 0x3C, 0x0B), (340, 3125, 0x3D, 0x0A), (343, 1250, 0x3E, 0x08),
(345, 9375, 0x3E, 0x07), (348, 7500, 0x3E, 0x06), (351, 5625, 0x3F, 0x04),
(354, 3750, 0x3F, 0x03), (357, 1875, 0x3F, 0x01),
]
assert len(GROUND_TRUTH) == 128, f"GROUND_TRUTH must have 128 entries, has {len(GROUND_TRUTH)}"
VM_I_REF: list[int] = [row[2] for row in GROUND_TRUTH]
VM_Q_REF: list[int] = [row[3] for row in GROUND_TRUTH]
# ----------------------------------------------------------------------------
# Structural-invariant checks on the embedded ground-truth transcription.
# These defend against typos during the copy-paste from the datasheet / ADI
# driver. Each function returns a list of error strings (empty == pass) so
# callers (the pytest class) can assert-on-empty with a useful message.
# ----------------------------------------------------------------------------
def check_byte_format(label: str, table: list[int]) -> list[str]:
"""Each byte must have bits[7:6] == 0 (reserved)."""
errors = []
for i, byte in enumerate(table):
if byte & 0xC0:
errors.append(f"{label}[{i}]=0x{byte:02X}: reserved bits[7:6] non-zero")
return errors
def check_uniform_2p8125_deg_step() -> list[str]:
"""Angles must form a uniform 2.8125 deg grid: angle[N] == N * 2.8125."""
errors = []
for i, (deg_int, deg_frac, _, _) in enumerate(GROUND_TRUTH):
# angle in units of 1/10000 degree; 2.8125 deg = 28125/10000 exactly
angle_e4 = deg_int * 10000 + deg_frac
expected_e4 = i * 28125
if angle_e4 != expected_e4:
errors.append(
f"GROUND_TRUTH[{i}]: angle {deg_int}.{deg_frac:04d} deg "
f"(={angle_e4}/10000) != expected {expected_e4}/10000 "
f"(=i*2.8125)"
)
return errors
def check_quadrant_symmetry() -> list[str]:
"""Angle and angle+180 deg must have inverted polarity bits but identical
magnitudes. Index offset 64 corresponds to 180 deg on the 128-step grid.
Exemption: when magnitude is zero the polarity bit is physically
meaningless (sign of zero is undefined for the IQ phasor projection).
The datasheet uses POL=1 for both 0 and 180 deg Q components (both
encode Q=0). Skip the polarity assertion for zero-magnitude entries.
"""
errors = []
POL = 0x20
MAG = 0x1F
for i in range(64):
j = i + 64
mag_i_a, mag_i_b = VM_I_REF[i] & MAG, VM_I_REF[j] & MAG
if mag_i_a != mag_i_b:
errors.append(
f"VM_I[{i}]=0x{VM_I_REF[i]:02X} vs VM_I[{j}]=0x{VM_I_REF[j]:02X}: "
f"180 deg pair has different magnitude"
)
if mag_i_a != 0 and (VM_I_REF[i] & POL) == (VM_I_REF[j] & POL):
errors.append(
f"VM_I[{i}]=0x{VM_I_REF[i]:02X} vs VM_I[{j}]=0x{VM_I_REF[j]:02X}: "
f"180 deg pair has same polarity (should be inverted, mag={mag_i_a})"
)
mag_q_a, mag_q_b = VM_Q_REF[i] & MAG, VM_Q_REF[j] & MAG
if mag_q_a != mag_q_b:
errors.append(
f"VM_Q[{i}]=0x{VM_Q_REF[i]:02X} vs VM_Q[{j}]=0x{VM_Q_REF[j]:02X}: "
f"180 deg pair has different magnitude"
)
if mag_q_a != 0 and (VM_Q_REF[i] & POL) == (VM_Q_REF[j] & POL):
errors.append(
f"VM_Q[{i}]=0x{VM_Q_REF[i]:02X} vs VM_Q[{j}]=0x{VM_Q_REF[j]:02X}: "
f"180 deg pair has same polarity (should be inverted, mag={mag_q_a})"
)
return errors
def check_cardinal_points() -> list[str]:
"""Spot-check cardinal phase points against datasheet expectations."""
errors = []
expectations = [
(0, 0x3F, 0x20, "0 deg: max +I, ~zero Q"),
(32, 0x21, 0x3D, "90 deg: ~zero I, max +Q"),
(64, 0x1F, 0x20, "180 deg: max -I, ~zero Q"),
(96, 0x01, 0x1D, "270 deg: ~zero I, max -Q"),
]
for idx, exp_i, exp_q, desc in expectations:
if VM_I_REF[idx] != exp_i or VM_Q_REF[idx] != exp_q:
errors.append(
f"index {idx} ({desc}): expected (0x{exp_i:02X}, 0x{exp_q:02X}), "
f"got (0x{VM_I_REF[idx]:02X}, 0x{VM_Q_REF[idx]:02X})"
)
return errors
# ----------------------------------------------------------------------------
# Parse VM_I[] / VM_Q[] from firmware C++ source.
# ----------------------------------------------------------------------------
ARRAY_RE = re.compile(
r"const\s+uint8_t\s+ADAR1000Manager::(?P<name>VM_I|VM_Q|VM_GAIN)\s*"
r"\[\s*128\s*\]\s*=\s*\{(?P<body>[^}]*)\}\s*;",
re.DOTALL,
)
HEX_RE = re.compile(r"0[xX][0-9a-fA-F]{1,2}")
def parse_array(source: str, name: str) -> list[int] | None:
"""Extract a 128-entry uint8_t array from C++ source by name.
Returns None if the array is not found. Returns a list (possibly shorter
than 128) of the parsed bytes if found; caller is responsible for length
validation.
LIMITATION (intentional, see PR fix/adar1000-vm-tables review finding #2):
ARRAY_RE uses `[^}]*` for the body, which terminates at the first `}`.
This is sufficient for the *flat* `const uint8_t NAME[128] = { ... };`
declarations VM_I/VM_Q use today, but it would mis-parse if the array
body ever contained nested braces (e.g. designated initialisers, struct
aggregates, or macro-expansions producing braces). If the firmware ever
needs such a form for the VM tables, replace ARRAY_RE with a balanced
brace-counting parser. Until then, the current regex is preferred for
its simplicity and the round-trip tests will catch any silent breakage.
"""
for m in ARRAY_RE.finditer(source):
if m.group("name") != name:
continue
body = m.group("body")
body = re.sub(r"//[^\n]*", "", body)
body = re.sub(r"/\*.*?\*/", "", body, flags=re.DOTALL)
return [int(tok, 16) for tok in HEX_RE.findall(body)]
return None
@@ -26,12 +26,14 @@ layers agree (because both could be wrong).
from __future__ import annotations
import ast
import os
import re
import struct
import subprocess
import tempfile
from pathlib import Path
from typing import ClassVar
import pytest
@@ -41,6 +43,7 @@ import sys
THIS_DIR = Path(__file__).resolve().parent
sys.path.insert(0, str(THIS_DIR))
import contract_parser as cp # noqa: E402
import adar1000_vm_reference as adar_vm # noqa: E402
# Also add the GUI dir to import radar_protocol
sys.path.insert(0, str(cp.GUI_DIR))
@@ -77,6 +80,78 @@ if _in_ci:
)
def _strip_cxx_comments_and_strings(src: str) -> str:
"""Return src with all C/C++ comments and string/char literals removed.
Tokenising state machine with four states:
* CODE — default; watches for `"`, `'`, `//`, `/*`
* STRING ("...") — handles `\\"` and `\\\\` escapes
* CHAR ('...') — handles `\\'` and `\\\\` escapes
* LINE_COMMENT — until next `\\n`
* BLOCK_COMMENT — until next `*/`
Used by test_vm_gain_table_is_not_reintroduced to ensure the substring
"VM_GAIN" appearing only inside an explanatory comment or a string
literal does NOT count as code reintroduction. We replace stripped
regions with a single space so token boundaries (and line counts, by
approximation — newlines preserved) are not collapsed.
"""
out: list[str] = []
i = 0
n = len(src)
CODE, STRING, CHAR, LINE_C, BLOCK_C = 0, 1, 2, 3, 4
state = CODE
while i < n:
c = src[i]
nxt = src[i + 1] if i + 1 < n else ""
if state == CODE:
if c == "/" and nxt == "/":
state = LINE_C
i += 2
elif c == "/" and nxt == "*":
state = BLOCK_C
i += 2
elif c == '"':
state = STRING
i += 1
elif c == "'":
state = CHAR
i += 1
else:
out.append(c)
i += 1
elif state == STRING:
if c == "\\" and i + 1 < n:
i += 2 # skip escape pair (handles \" and \\)
elif c == '"':
state = CODE
i += 1
else:
i += 1
elif state == CHAR:
if c == "\\" and i + 1 < n:
i += 2
elif c == "'":
state = CODE
i += 1
else:
i += 1
elif state == LINE_C:
if c == "\n":
out.append("\n") # preserve line numbering
state = CODE
i += 1
elif state == BLOCK_C:
if c == "*" and nxt == "/":
state = CODE
i += 2
else:
if c == "\n":
out.append("\n")
i += 1
return "".join(out)
def _parse_hex_results(text: str) -> list[dict[str, str]]:
"""Parse space-separated hex lines from TB output files."""
rows = []
@@ -491,7 +566,7 @@ class TestTier1AgcCrossLayerInvariant:
MCU must apply a 2-frame confirmation debounce before mutating
outerAgc.enabled from DIG_6 reads. A naive assignment straight from
the latest GPIO sample would let a single-cycle glitch flip the AGC
state for one frame.
state for one frame — defeating the debounce claim in the PR body.
"""
main_cpp = (cp.MCU_CODE_DIR / "main.cpp").read_text()
@@ -552,6 +627,420 @@ class TestTier1AgcCrossLayerInvariant:
)
# ===================================================================
# ADAR1000 channel→register round-trip invariant (issue #90)
# ===================================================================
#
# Ground-truth invariant crossing three system layers:
# Chip (datasheet) -> Driver (MCU helpers) -> Application (callers).
#
# For every logical element ch in {0,1,2,3} (hardware channels CH1..CH4),
# the round-trip
# caller_expr(ch) --> helper_offset(channel) * stride --> base + off
# must land on the physical register REG_CH{ch+1}_* defined in the ADI
# ADAR1000 register map parsed from ADAR1000_Manager.h.
#
# Catches:
# * #90 channel rotation regardless of which side is fixed (caller OR helper).
# * Wrong stride (e.g. phase written with stride 1 instead of 2).
# * Bad mask (e.g. `channel & 0x07`, `channel & 0x01`).
# * Wrong base register in a helper.
# * New setter added with mismatched convention.
# * Caller moved to a file the test no longer scans (fails loudly).
#
# Cannot be defeated by:
# * Renaming/refactoring helper layout: the setter coverage test
# (`test_helper_sites_exist_for_all_setters`) catches missing parse.
# * Changing 0x03 to 3 or adding a named constant: the offset is
# evaluated symbolically via AST, not matched by regex.
def _parse_adar_register_map(header_text):
"""Extract `#define REG_CHn_(RX|TX)_(GAIN|PHS_I|PHS_Q)` values."""
regs = {}
for m in re.finditer(
r"^#define\s+(REG_CH[1-4]_(?:RX|TX)_(?:GAIN|PHS_I|PHS_Q))\s+(0x[0-9A-Fa-f]+)",
header_text,
re.MULTILINE,
):
regs[m.group(1)] = int(m.group(2), 16)
return regs
def _safe_eval_int_expr(expr, **variables):
"""
Evaluate a small integer expression with +, -, *, &, |, ^, ~, <<, >>.
Python's & / | / ^ / ~ / << / >> have the same semantics as C for the
operand widths we care about here (uint8_t after the mask makes the
result fit in 0..3). No floating point, no function calls, no names
outside ``variables``.
SECURITY: ``expr`` MUST come from a trusted source -- specifically,
C/C++ source text under version control in this repository (e.g.
arguments parsed out of ``main.cpp``/``ADAR1000_AGC.cpp``). Although
the AST whitelist below rejects function calls, attribute access,
subscripts, and any name not in ``variables``, ``eval`` is still
invoked on the compiled tree. Do NOT pass user-supplied / network /
GUI input here.
"""
tree = ast.parse(expr, mode="eval")
allowed = (
ast.Expression, ast.BinOp, ast.UnaryOp, ast.Constant,
ast.Name, ast.Load,
ast.Add, ast.Sub, ast.Mult, ast.Mod, ast.FloorDiv,
ast.BitAnd, ast.BitOr, ast.BitXor,
ast.USub, ast.UAdd, ast.Invert,
ast.LShift, ast.RShift,
)
for node in ast.walk(tree):
if not isinstance(node, allowed):
raise ValueError(
f"disallowed AST node {type(node).__name__!s} in `{expr}`"
)
return eval(
compile(tree, "<expr>", "eval"),
{"__builtins__": {}},
variables,
)
def _extract_adar_helper_sites(manager_cpp, setter_names):
"""
For each setter, locate the body of ``void ADAR1000Manager::<setter>``
and return a list of (setter, base_register, offset_expr_c, stride)
for every ``REG_CHn_XXX + <expr>`` memory-address assignment.
"""
sites = []
for setter in setter_names:
m = re.search(
rf"void\s+ADAR1000Manager::{setter}\s*\([^)]*\)\s*\{{(.+?)^\}}",
manager_cpp,
re.MULTILINE | re.DOTALL,
)
if not m:
continue
body = m.group(1)
for access in re.finditer(
r"=\s*(REG_CH[1-4]_(?:RX|TX)_(?:GAIN|PHS_I|PHS_Q))\s*\+\s*([^;]+);",
body,
):
base = access.group(1)
rhs = access.group(2).strip()
# Trailing `* <integer>` = stride multiplier (2 for phase I/Q).
stride_match = re.match(r"(.+?)\s*\*\s*(\d+)\s*$", rhs)
if stride_match:
offset_expr = stride_match.group(1).strip()
stride = int(stride_match.group(2))
else:
offset_expr = rhs
stride = 1
sites.append((setter, base, offset_expr, stride))
return sites
# Method-definition line pattern: `[qualifier...] <ret-type> <Class>::<setter>(`
# Covers: plain `void X::f(`, `inline void X::f(`, `static bool X::f(`, etc.
_DEFN_RE = re.compile(
r"^\s*(?:inline\s+|static\s+|virtual\s+|constexpr\s+|explicit\s+)*"
r"(?:void|bool|uint\w+|int\w*|auto)\s+\S+::\w+\s*\("
)
def _extract_adar_caller_sites(sources, setter):
"""
Find every call ``<obj>.<setter>(dev, <channel_expr>, ...)`` across
``sources = [(filename, text), ...]``. Returns (filename, line_no,
channel_expr) for each. Skips function declarations/definitions.
Arg list up to matching `)`: restricted to a single line. All existing
call sites fit on one line; a future multi-line refactor would drop
callers from the scan, which the round-trip test surfaces loudly via
`assert callers` (rather than silently missing a site).
"""
out = []
call_re = re.compile(rf"\b{setter}\s*\(([^;]*?)\)\s*;")
for filename, text in sources:
for line_no, line in enumerate(text.splitlines(), start=1):
# Skip method definition / declaration lines.
if _DEFN_RE.match(line):
continue
cm = call_re.search(line)
if not cm:
continue
args = _split_top_level_commas(cm.group(1))
if len(args) < 2:
continue
channel_expr = args[1].strip()
out.append((filename, line_no, channel_expr))
return out
def _split_top_level_commas(text):
"""Split on commas that sit at paren-depth 0 (ignores nested calls)."""
parts, depth, cur = [], 0, []
for ch in text:
if ch == "(":
depth += 1
cur.append(ch)
elif ch == ")":
depth -= 1
cur.append(ch)
elif ch == "," and depth == 0:
parts.append("".join(cur))
cur = []
else:
cur.append(ch)
if cur:
parts.append("".join(cur))
return parts
class TestTier1Adar1000ChannelRegisterRoundTrip:
"""
Cross-layer round-trip: caller channel expr -> helper offset formula
-> physical register address must equal REG_CH{ch+1}_* for every
caller and every ch in {0,1,2,3}.
See module-level block comment above and upstream issue #90.
"""
_SETTERS = (
"adarSetRxPhase",
"adarSetTxPhase",
"adarSetRxVgaGain",
"adarSetTxVgaGain",
)
# Register base -> stride override. Parsed values of stride are
# trusted; this table is the independent ground truth for cross-check.
_EXPECTED_STRIDE: ClassVar[dict[str, int]] = {
"REG_CH1_RX_GAIN": 1,
"REG_CH1_TX_GAIN": 1,
"REG_CH1_RX_PHS_I": 2,
"REG_CH1_RX_PHS_Q": 2,
"REG_CH1_TX_PHS_I": 2,
"REG_CH1_TX_PHS_Q": 2,
}
@classmethod
def setup_class(cls):
cls.header_txt = (cp.MCU_LIB_DIR / "ADAR1000_Manager.h").read_text()
cls.manager_txt = (cp.MCU_LIB_DIR / "ADAR1000_Manager.cpp").read_text()
cls.reg_map = _parse_adar_register_map(cls.header_txt)
cls.helper_sites = _extract_adar_helper_sites(
cls.manager_txt, cls._SETTERS,
)
# Auto-discover every C++ TU under the MCU tree so a new caller
# added to e.g. a future ``ADAR1000_Calibration.cpp`` cannot
# silently escape the round-trip check (issue #90 reviewer note).
# Exclude any path containing a ``tests`` segment so this test
# does not parse its own fixtures. The resulting list is
# deterministic (sorted) for reproducible parametrization.
scanned = []
seen = set()
for root in (cp.MCU_LIB_DIR, cp.MCU_CODE_DIR):
for path in sorted(root.rglob("*.cpp")):
if "tests" in path.parts:
continue
if path in seen:
continue
seen.add(path)
scanned.append((path.name, path.read_text()))
cls.sources = scanned
# Sanity: the two TUs known to call ADAR1000 setters at the time
# of issue #90 must be in scope. If a future refactor renames or
# moves them this assert fires loudly rather than silently
# passing an empty round-trip.
scanned_names = {n for (n, _) in scanned}
for required in ("ADAR1000_AGC.cpp", "main.cpp", "ADAR1000_Manager.cpp"):
assert required in scanned_names, (
f"Auto-discovery missed `{required}`; check MCU_LIB_DIR / "
f"MCU_CODE_DIR roots in contract_parser.py."
)
# ---------- Tier A: chip ground truth ----------------------------
def test_register_map_gain_stride_is_one_per_channel(self):
"""Datasheet invariant: RX/TX VGA gain registers are 1 byte apart."""
for kind in ("RX_GAIN", "TX_GAIN"):
for n in range(1, 4):
delta = (
self.reg_map[f"REG_CH{n+1}_{kind}"]
- self.reg_map[f"REG_CH{n}_{kind}"]
)
assert delta == 1, (
f"ADAR1000 register map invariant broken: "
f"REG_CH{n+1}_{kind} - REG_CH{n}_{kind} = {delta}, "
f"datasheet says 1. Either the header was mis-edited "
f"or ADI released a part with a different map."
)
def test_register_map_phase_stride_is_two_per_channel(self):
"""Datasheet invariant: phase I/Q pairs occupy 2 bytes per channel."""
for kind in ("RX_PHS_I", "RX_PHS_Q", "TX_PHS_I", "TX_PHS_Q"):
for n in range(1, 4):
delta = (
self.reg_map[f"REG_CH{n+1}_{kind}"]
- self.reg_map[f"REG_CH{n}_{kind}"]
)
assert delta == 2, (
f"ADAR1000 register map invariant broken: "
f"REG_CH{n+1}_{kind} - REG_CH{n}_{kind} = {delta}, "
f"datasheet says 2."
)
# ---------- Tier B: driver parses cleanly -------------------------
def test_helper_sites_exist_for_all_setters(self):
"""Every channel-indexed setter must parse at least one register access."""
found = {s for (s, _, _, _) in self.helper_sites}
missing = set(self._SETTERS) - found
assert not missing, (
f"Helper parse failed for: {sorted(missing)}. "
f"Either a setter was renamed (update _SETTERS), moved out of "
f"ADAR1000_Manager.cpp (extend scan scope), or the register-"
f"access form changed beyond `REG_CHn_XXX + <expr>`. "
f"DO NOT weaken this test without reviewing issue #90."
)
def test_helper_parsed_stride_matches_datasheet(self):
"""Parsed helper strides must match the datasheet register spacing."""
for setter, base, offset_expr, stride in self.helper_sites:
expected = self._EXPECTED_STRIDE.get(base)
assert expected is not None, (
f"{setter} writes to unrecognised base `{base}`. "
f"If ADI added a new channel-indexed register block, "
f"extend _EXPECTED_STRIDE with its datasheet stride."
)
assert stride == expected, (
f"{setter} helper uses stride {stride} for `{base}` "
f"(`{offset_expr} * {stride}`), datasheet says {expected}. "
f"Writes will overlap or skip channels."
)
# ---------- Tier C: round-trip to physical register ---------------
def test_all_callers_pass_one_based_channel(self):
"""
INVARIANT: every caller's channel argument must, for ch in
{0,1,2,3}, evaluate to a 1-based ADI channel index in {1,2,3,4}.
The bug fixed in #90 was that helpers used ``channel & 0x03``
directly, so a caller passing bare ``ch`` (0..3) appeared to
work for ch=0..2 and silently aliased ch=3 onto CH4-then-CH1.
After the fix, helpers do ``(channel - 1) & 0x03`` and reject
``channel < 1 || channel > 4``. A future caller written as
``adarSetRxPhase(dev, ch, ...)`` (bare 0-based) or
``adarSetRxPhase(dev, 0, ...)`` (literal 0) would silently be
dropped by the bounds-check at runtime; this test catches it at
CI time instead.
The check intentionally lives one tier above the round-trip test
so the failure message points the reader at the API contract
(1-based per ADI datasheet & ADAR1000_AGC.cpp:76) rather than at
a register-arithmetic mismatch.
"""
offenders = []
for setter in self._SETTERS:
callers = _extract_adar_caller_sites(self.sources, setter)
for filename, line_no, ch_expr in callers:
for ch in range(4):
try:
channel_val = _safe_eval_int_expr(ch_expr, ch=ch)
except (NameError, KeyError, ValueError) as e:
offenders.append(
f" - {filename}:{line_no} {setter}("
f"…, `{ch_expr}`, …) -- ch={ch}: "
f"unparseable ({e})"
)
continue
if channel_val not in (1, 2, 3, 4):
offenders.append(
f" - {filename}:{line_no} {setter}("
f"…, `{ch_expr}`, …) -- ch={ch}: "
f"channel={channel_val}, expected 1..4"
)
assert not offenders, (
"ADAR1000 1-based channel API contract violated. The fix "
"for issue #90 requires every caller to pass channel in "
"{1,2,3,4} (CH1..CH4 per ADI datasheet). Bare 0-based ch "
"or a literal 0 will be silently dropped by the helper's "
"bounds check. Offenders:\n" + "\n".join(offenders)
)
@pytest.mark.parametrize(
"setter",
[
"adarSetRxPhase",
"adarSetTxPhase",
"adarSetRxVgaGain",
"adarSetTxVgaGain",
],
)
def test_round_trip_lands_on_intended_physical_channel(self, setter):
"""
INVARIANT: for every caller of ``<setter>`` and every logical ch
in {0,1,2,3}, the effective register address equals
REG_CH{ch+1}_*. Catches #90 regardless of fix direction.
"""
callers = _extract_adar_caller_sites(self.sources, setter)
assert callers, (
f"No callers of `{setter}` found. Either the test scope is "
f"incomplete (extend `setup_class.sources`) or the symbol was "
f"inlined/removed. A blind test is a dangerous test — "
f"investigate before weakening."
)
helpers = [
(b, e, s) for (nm, b, e, s) in self.helper_sites if nm == setter
]
assert helpers, f"helper body for `{setter}` not parseable"
errors = []
for filename, line_no, ch_expr in callers:
for ch in range(4):
try:
channel_val = _safe_eval_int_expr(ch_expr, ch=ch)
except (NameError, KeyError, ValueError) as e:
pytest.fail(
f"{filename}:{line_no}: caller channel expression "
f"`{ch_expr}` uses symbol outside {{ch}} or a "
f"disallowed operator ({e}). Extend "
f"_safe_eval_int_expr variables or rewrite the "
f"call site with a supported expression."
)
for base_sym, offset_expr, stride in helpers:
try:
offset = _safe_eval_int_expr(
offset_expr, channel=channel_val,
)
except (NameError, KeyError, ValueError) as e:
pytest.fail(
f"helper `{setter}` offset expr "
f"`{offset_expr}` uses symbol outside "
f"{{channel}} or a disallowed operator ({e}). "
f"Extend _safe_eval_int_expr variables if new "
f"driver state is introduced."
)
final = self.reg_map[base_sym] + offset * stride
expected_sym = base_sym.replace("CH1", f"CH{ch + 1}")
expected = self.reg_map[expected_sym]
if final != expected:
errors.append(
f" - {filename}:{line_no} {setter} "
f"caller `{ch_expr}` | ch={ch} -> "
f"channel={channel_val} -> "
f"`{base_sym} + ({offset_expr})"
f"{' * ' + str(stride) if stride != 1 else ''}`"
f" = 0x{final:03X} "
f"(expected {expected_sym} = 0x{expected:03X})"
)
assert not errors, (
f"ADAR1000 channel round-trip FAILED for {setter} "
f"({len(errors)} mismatches) — writes routed to wrong physical "
f"channel. This is issue #90.\n" + "\n".join(errors)
)
class TestTier1DataPacketLayout:
"""Verify data packet byte layout matches between Python and Verilog."""
@@ -665,6 +1154,204 @@ class TestTier1STM32SettingsPacket:
assert flag == [23, 46, 158, 237], f"Start flag: {flag}"
# ===================================================================
# TIER 2: ADAR1000 Vector Modulator Lookup-Table Ground Truth
# ===================================================================
#
# Cross-layer contract: the firmware constants
# ADAR1000Manager::VM_I[128] / VM_Q[128]
# (in 9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/ADAR1000_Manager.cpp)
# MUST equal the byte values published in the ADAR1000 datasheet Rev. B,
# Tables 13-16 page 34 ("Phase Shifter Programming"), on a uniform 2.8125 deg
# grid (index N == phase N * 360/128 deg).
#
# Independent ground truth lives in tools/verify_adar1000_vm_tables.py
# (transcribed from the datasheet, cross-checked against the ADI Linux
# beamformer driver as a secondary source). This test imports that
# reference and asserts a byte-exact match.
#
# Historical bug guarded against: from initial commit through PR #94 the
# arrays shipped as empty placeholders ("// ... (same as in your original
# file)"), so every adarSetRxPhase / adarSetTxPhase call wrote I=Q=0 and
# beam steering was non-functional. A separate VM_GAIN[128] table was
# declared but never read anywhere; this test also enforces its removal so
# it cannot be reintroduced and silently shadow real bugs.
class TestTier2Adar1000VmTableGroundTruth:
"""Firmware ADAR1000 VM_I/VM_Q must match datasheet ground truth byte-exact."""
@pytest.fixture(scope="class")
def cpp_source(self):
path = (
cp.REPO_ROOT
/ "9_Firmware"
/ "9_1_Microcontroller"
/ "9_1_1_C_Cpp_Libraries"
/ "ADAR1000_Manager.cpp"
)
assert path.is_file(), f"Firmware source missing: {path}"
return path.read_text()
def test_ground_truth_table_shape(self):
"""Sanity-check the imported reference (defends against import-path mishap)."""
gt = adar_vm.GROUND_TRUTH
assert len(gt) == 128, "Ground-truth table must have exactly 128 entries"
# Each row is (deg_int, deg_frac_e4, vm_i_byte, vm_q_byte)
for k, row in enumerate(gt):
assert len(row) == 4, f"Row {k} malformed: {row}"
assert 0 <= row[2] <= 0xFF, f"VM_I[{k}] out of byte range: {row[2]:#x}"
assert 0 <= row[3] <= 0xFF, f"VM_Q[{k}] out of byte range: {row[3]:#x}"
# Byte format: bits[7:6] reserved zero, bits[5] polarity, bits[4:0] mag
assert (row[2] & 0xC0) == 0, f"VM_I[{k}] reserved bits set: {row[2]:#x}"
assert (row[3] & 0xC0) == 0, f"VM_Q[{k}] reserved bits set: {row[3]:#x}"
def test_ground_truth_byte_format(self):
"""Transcription self-check: every VM_I/VM_Q byte has reserved bits clear."""
errors = adar_vm.check_byte_format("VM_I_REF", adar_vm.VM_I_REF)
errors += adar_vm.check_byte_format("VM_Q_REF", adar_vm.VM_Q_REF)
assert not errors, (
"Byte-format violations in embedded GROUND_TRUTH (likely transcription "
"typo from ADAR1000 datasheet Tables 13-16):\n " + "\n ".join(errors)
)
def test_ground_truth_uniform_2p8125_deg_grid(self):
"""Transcription self-check: angles form a uniform 2.8125 deg grid.
This is the assumption that lets the firmware use `VM_*[phase % 128]`
as a direct index (no nearest-neighbour search). If the embedded
angles drift off the grid, the firmware's indexing model is wrong.
"""
errors = adar_vm.check_uniform_2p8125_deg_step()
assert not errors, (
"Non-uniform angle grid in GROUND_TRUTH:\n " + "\n ".join(errors)
)
def test_ground_truth_quadrant_symmetry(self):
"""Transcription self-check: phi and phi+180 deg have same magnitude,
opposite polarity. Catches swapped/rotated rows in the table.
"""
errors = adar_vm.check_quadrant_symmetry()
assert not errors, (
"Quadrant-symmetry violation in GROUND_TRUTH (table rows may be "
"transposed or mis-transcribed):\n " + "\n ".join(errors)
)
def test_ground_truth_cardinal_points(self):
"""Transcription self-check: the four cardinal phases (0, 90, 180,
270 deg) match the datasheet-published extrema exactly.
"""
errors = adar_vm.check_cardinal_points()
assert not errors, (
"Cardinal-point mismatch in GROUND_TRUTH vs ADAR1000 datasheet "
"Tables 13-16:\n " + "\n ".join(errors)
)
def test_firmware_vm_i_matches_datasheet(self, cpp_source):
gt = adar_vm.GROUND_TRUTH
firmware = adar_vm.parse_array(cpp_source, "VM_I")
assert firmware is not None, (
"Could not parse VM_I[128] from ADAR1000_Manager.cpp; "
"definition pattern may have drifted"
)
assert len(firmware) == 128, (
f"VM_I has {len(firmware)} entries, expected 128. "
"Empty placeholder regression — every phase write would emit I=0 "
"and beam steering would be silently broken."
)
mismatches = [
(k, firmware[k], gt[k][2])
for k in range(128)
if firmware[k] != gt[k][2]
]
assert not mismatches, (
f"VM_I diverges from datasheet at {len(mismatches)} indices; "
f"first 5: {mismatches[:5]}"
)
def test_firmware_vm_q_matches_datasheet(self, cpp_source):
gt = adar_vm.GROUND_TRUTH
firmware = adar_vm.parse_array(cpp_source, "VM_Q")
assert firmware is not None, (
"Could not parse VM_Q[128] from ADAR1000_Manager.cpp; "
"definition pattern may have drifted"
)
assert len(firmware) == 128, (
f"VM_Q has {len(firmware)} entries, expected 128. "
"Empty placeholder regression — every phase write would emit Q=0."
)
mismatches = [
(k, firmware[k], gt[k][3])
for k in range(128)
if firmware[k] != gt[k][3]
]
assert not mismatches, (
f"VM_Q diverges from datasheet at {len(mismatches)} indices; "
f"first 5: {mismatches[:5]}"
)
def test_vm_gain_table_is_not_reintroduced(self, cpp_source):
"""Dead-code regression guard: VM_GAIN[128] must not exist as code.
The ADAR1000 vector modulator has no separate gain register; magnitude
is bits[4:0] of the I/Q bytes themselves. Per-channel VGA gain uses
registers CHx_RX_GAIN (0x10-0x13) / CHx_TX_GAIN (0x1C-0x1F) written
directly by adarSetRxVgaGain / adarSetTxVgaGain. A VM_GAIN[] array
was declared in early development, never populated, never read, and
was removed in PR fix/adar1000-vm-tables. Reintroducing it would
suggest (falsely) that an extra lookup is needed and could mask the
real signal path.
Uses a tokenising comment/string stripper so that the historical
explanation comment in the cpp file, as well as any string literal
containing the substring "VM_GAIN", does not trip the check.
"""
stripped = _strip_cxx_comments_and_strings(cpp_source)
assert "VM_GAIN" not in stripped, (
"VM_GAIN symbol reappeared in ADAR1000_Manager.cpp executable code. "
"This array has no hardware backing and must not be reintroduced. "
"If you need to scale phase-state magnitude, modify VM_I/VM_Q "
"bits[4:0] directly per the datasheet."
)
def test_adversarial_corruption_is_detected(self):
"""Adversarial self-test: a flipped byte in firmware MUST fail comparison.
Defends against silent bypass — e.g. a future refactor that mocks
parse_array() or compares len() only. We synthesise a corrupted cpp
source string, run the same parser, and assert mismatch is detected.
"""
gt = adar_vm.GROUND_TRUTH
# Build a minimal valid-looking cpp snippet with one corrupted byte.
good_i = ", ".join(f"0x{gt[k][2]:02X}" for k in range(128))
good_q = ", ".join(f"0x{gt[k][3]:02X}" for k in range(128))
snippet_good = (
f"const uint8_t ADAR1000Manager::VM_I[128] = {{ {good_i} }};\n"
f"const uint8_t ADAR1000Manager::VM_Q[128] = {{ {good_q} }};\n"
)
# Sanity: the unmodified snippet must parse and match.
parsed_i = adar_vm.parse_array(snippet_good, "VM_I")
assert parsed_i is not None and len(parsed_i) == 128
assert all(parsed_i[k] == gt[k][2] for k in range(128)), (
"Self-test setup error: golden snippet does not match GROUND_TRUTH"
)
# Now flip the low bit of VM_I[42] and confirm detection.
corrupted_byte = gt[42][2] ^ 0x01
bad_i = ", ".join(
f"0x{(corrupted_byte if k == 42 else gt[k][2]):02X}"
for k in range(128)
)
snippet_bad = (
f"const uint8_t ADAR1000Manager::VM_I[128] = {{ {bad_i} }};\n"
f"const uint8_t ADAR1000Manager::VM_Q[128] = {{ {good_q} }};\n"
)
parsed_bad = adar_vm.parse_array(snippet_bad, "VM_I")
assert parsed_bad is not None and len(parsed_bad) == 128
assert parsed_bad[42] != gt[42][2], (
"Adversarial self-test FAILED: corrupted byte at index 42 was "
"not detected by parse_array. The cross-layer test is bypassable."
)
# ===================================================================
# TIER 2: Verilog Cosimulation
# ===================================================================
+4 -4
View File
@@ -68,13 +68,13 @@ The AERIS-10 main sub-systems are:
- Clock Generator (AD9523-1)
- 2x Frequency Synthesizers (ADF4382)
- 4x 4-Channel Phase Shifters (ADAR1000) for RADAR pulse sequencing
- 2x ADS7830 ADCs (on Power Amplifier Boards) for Idq measurement
- 2x DAC5578 (on Power Amplifier Boards) for Vg control
- GPS module for GUI map centering
- 2x ADS7830 8-channel I²C ADCs (Main Board, U88 @ 0x48 / U89 @ 0x4A) for 16x Idq measurement, one per PA channel, each sensed through a 5 mΩ shunt on the PA board and an INA241A3 current-sense amplifier (x50) on the Main Board
- 2x DAC5578 8-channel I²C DACs (Main Board, U7 @ 0x48 / U69 @ 0x49) for 16x Vg control, one per PA channel; closed-loop calibrated at boot to the target Idq
- GPS module (UM982) for GUI map centering and per-detection position tagging
- GY-85 IMU for pitch/roll correction of target coordinates
- BMP180 Barometer
- Stepper Motor
- 8x ADS7830 Temperature Sensors for cooling fan control
- 1x ADS7830 8-channel I²C ADC (Main Board, U10) reading 8 thermistors for thermal monitoring; a single GPIO (EN_DIS_COOLING) switches the cooling fans on when any channel exceeds the threshold
- RF switches
- **16x Power Amplifier Boards** - Used only for AERIS-10E version, featuring 10Watt QPA2962 GaN amplifier for extended range