Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 27b55f37dc | |||
| 582476fa0d | |||
| 7c91a3e0b9 | |||
| fd6cff5b2b | |||
| 964f1903f3 | |||
| 12b549dafb | |||
| 5d5e9ff297 | |||
| 754d919e44 | |||
| 0443516cc9 | |||
| 5fbe0513b5 | |||
| c3db8a9122 | |||
| ec8256e25a | |||
| 8e1b3f22d2 | |||
| 15ae940be5 | |||
| 658752abb7 | |||
| fa5e1dcdf4 | |||
| ade1497457 | |||
| f1d3bff4fe | |||
| 791b2e7374 | |||
| df875bdf4d | |||
| 15a9cde274 | |||
| ae7643975d | |||
| 8609e455a0 | |||
| 029df375f5 | |||
| a9ceb3c851 | |||
| 425c349184 | |||
| bcbbfabbdb | |||
| b9c36dcca5 | |||
| db4e73577e | |||
| 35539ea934 | |||
| 8187771ab0 | |||
| b0e5b298fe | |||
| f67440ee9a | |||
| 513e0b9a69 | |||
| 78dff2fd3d | |||
| 0b25db08b5 | |||
| 4900282042 | |||
| 3f4513fec2 | |||
| a2686b7424 | |||
| cf3d288268 | |||
| 1c7861bb0d | |||
| 6bde91298d |
Binary file not shown.
@@ -550,7 +550,7 @@
|
|||||||
<text x="3.085225" y="81.68279375" size="1.778" layer="51">GND</text>
|
<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="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="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="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="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>
|
<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.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.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="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="253.964015625" y="102.099125" 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="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="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="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="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="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>
|
<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="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"/>
|
<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="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="182.675396875" y="267.73684375" 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="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="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="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="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="249.695853125" y="96.471690625" 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.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="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="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>
|
<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="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="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="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="51.802165625" y="131.052934375" 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="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="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.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>
|
<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="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.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="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="68.73355625" y="72.201796875" 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="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="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="45.798875" y="84.61879375" size="1.778" layer="51" rot="R180">JP2</text>
|
||||||
<text x="43.95" y="88.9" size="1.778" layer="51">JP8</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">JP7</text>
|
<text x="29.1" y="93.2" size="1.778" layer="51">IMU</text>
|
||||||
<text x="21.75" y="85.35" size="1.778" layer="51">JP18</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="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="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.1909375" y="71.621040625" size="1.778" layer="51">JP10</text>
|
||||||
<text x="62.9" y="82.75" size="1.778" layer="51">JP10</text>
|
<text x="54.996875" y="70.359128125" size="1.2" layer="51">STEPPER</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.27" layer="51" rot="R270">GND</text>
|
||||||
<text x="43.9" y="78.65" size="1.778" layer="51" rot="R270">GND</text>
|
<text x="52.61158125" y="88.897171875" size="1.016" layer="51" rot="R90">GND</text>
|
||||||
<text x="53.95" y="86.4" size="1.778" layer="51">GND</text>
|
|
||||||
<text x="31.3" y="84.75" size="1.778" layer="51" rot="R270">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>
|
<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"/>
|
<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="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="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="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>
|
</plain>
|
||||||
<libraries>
|
<libraries>
|
||||||
<library name="eagle-ltspice">
|
<library name="eagle-ltspice">
|
||||||
@@ -24576,8 +24625,8 @@ Your PCBWay Team
|
|||||||
<vertex x="114" y="112" curve="-180"/>
|
<vertex x="114" y="112" curve="-180"/>
|
||||||
</polygon>
|
</polygon>
|
||||||
<polygon width="0.254" layer="1" spacing="5.08">
|
<polygon width="0.254" layer="1" spacing="5.08">
|
||||||
<vertex x="258.75" y="116" curve="-180"/>
|
<vertex x="258.9164" y="116.0208" curve="-180"/>
|
||||||
<vertex x="254.75" y="112" curve="-180"/>
|
<vertex x="254.9164" y="112.0208" curve="-180"/>
|
||||||
</polygon>
|
</polygon>
|
||||||
<polygon width="0.254" layer="1" spacing="5.08">
|
<polygon width="0.254" layer="1" spacing="5.08">
|
||||||
<vertex x="260" y="300"/>
|
<vertex x="260" y="300"/>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ ADAR1000_AGC::ADAR1000_AGC()
|
|||||||
, min_gain(0)
|
, min_gain(0)
|
||||||
, max_gain(127)
|
, max_gain(127)
|
||||||
, holdoff_frames(4)
|
, holdoff_frames(4)
|
||||||
, enabled(true)
|
, enabled(false)
|
||||||
, holdoff_counter(0)
|
, holdoff_counter(0)
|
||||||
, last_saturated(false)
|
, last_saturated(false)
|
||||||
, saturation_event_count(0)
|
, saturation_event_count(0)
|
||||||
|
|||||||
@@ -20,18 +20,71 @@ static const struct {
|
|||||||
{ADAR_4_CS_3V3_GPIO_Port, ADAR_4_CS_3V3_Pin} // ADAR1000 #4
|
{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] = {
|
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] = {
|
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] = {
|
// NOTE: a VM_GAIN[128] table previously existed here as a placeholder but was
|
||||||
// ... (same as in your original file)
|
// 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() {
|
ADAR1000Manager::ADAR1000Manager() {
|
||||||
for (int i = 0; i < 4; ++i) {
|
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) {
|
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 i_val = VM_I[phase % 128];
|
||||||
uint8_t q_val = VM_Q[phase % 128];
|
uint8_t q_val = VM_Q[phase % 128];
|
||||||
|
|
||||||
uint32_t mem_addr_i = REG_CH1_RX_PHS_I + (channel & 0x03) * 2;
|
// Subtract 1 to convert 1-based channel to 0-based register offset
|
||||||
uint32_t mem_addr_q = REG_CH1_RX_PHS_Q + (channel & 0x03) * 2;
|
// 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_i, i_val, broadcast);
|
||||||
adarWrite(deviceIndex, mem_addr_q, q_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) {
|
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 i_val = VM_I[phase % 128];
|
||||||
uint8_t q_val = VM_Q[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_i = REG_CH1_TX_PHS_I + ((channel - 1) & 0x03) * 2;
|
||||||
uint32_t mem_addr_q = REG_CH1_TX_PHS_Q + (channel & 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_i, i_val, broadcast);
|
||||||
adarWrite(deviceIndex, mem_addr_q, q_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) {
|
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, mem_addr, gain, broadcast);
|
||||||
adarWrite(deviceIndex, REG_LOAD_WORKING, 0x1, broadcast);
|
adarWrite(deviceIndex, REG_LOAD_WORKING, 0x1, broadcast);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ADAR1000Manager::adarSetTxVgaGain(uint8_t deviceIndex, uint8_t channel, uint8_t gain, uint8_t 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, mem_addr, gain, broadcast);
|
||||||
adarWrite(deviceIndex, REG_LOAD_WORKING, LD_WRK_REGS_LDTX_OVERRIDE, broadcast);
|
adarWrite(deviceIndex, REG_LOAD_WORKING, LD_WRK_REGS_LDTX_OVERRIDE, broadcast);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,10 +116,12 @@ public:
|
|||||||
bool beam_sweeping_active_ = false;
|
bool beam_sweeping_active_ = false;
|
||||||
uint32_t last_beam_update_time_ = 0;
|
uint32_t last_beam_update_time_ = 0;
|
||||||
|
|
||||||
// Lookup tables
|
// Vector Modulator lookup tables (see ADAR1000_Manager.cpp for provenance).
|
||||||
static const uint8_t VM_I[128];
|
// 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_Q[128];
|
||||||
static const uint8_t VM_GAIN[128];
|
|
||||||
|
|
||||||
// Named defaults for the ADTR1107 and ADAR1000 power sequence.
|
// Named defaults for the ADTR1107 and ADAR1000 power sequence.
|
||||||
static constexpr uint8_t kDefaultTxVgaGain = 0x7F;
|
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 "usb_device.h"
|
||||||
#include "USBHandler.h"
|
#include "USBHandler.h"
|
||||||
#include "usbd_cdc_if.h"
|
#include "usbd_cdc_if.h"
|
||||||
#include "adar1000.h"
|
|
||||||
#include "ADAR1000_Manager.h"
|
#include "ADAR1000_Manager.h"
|
||||||
#include "ADAR1000_AGC.h"
|
#include "ADAR1000_AGC.h"
|
||||||
extern "C" {
|
extern "C" {
|
||||||
@@ -46,7 +45,9 @@ extern "C" {
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include "stm32_spi.h"
|
#include "stm32_spi.h"
|
||||||
#include "stm32_delay.h"
|
#include "stm32_delay.h"
|
||||||
#include "TinyGPSPlus.h"
|
extern "C" {
|
||||||
|
#include "um982_gps.h"
|
||||||
|
}
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include "GY_85_HAL.h"
|
#include "GY_85_HAL.h"
|
||||||
}
|
}
|
||||||
@@ -121,8 +122,8 @@ UART_HandleTypeDef huart5;
|
|||||||
UART_HandleTypeDef huart3;
|
UART_HandleTypeDef huart3;
|
||||||
|
|
||||||
/* USER CODE BEGIN PV */
|
/* USER CODE BEGIN PV */
|
||||||
// The TinyGPSPlus object
|
// UM982 dual-antenna GPS receiver
|
||||||
TinyGPSPlus gps;
|
UM982_GPS_t um982;
|
||||||
|
|
||||||
// Global data structures
|
// Global data structures
|
||||||
GPS_Data_t current_gps_data = {0};
|
GPS_Data_t current_gps_data = {0};
|
||||||
@@ -173,7 +174,7 @@ float RADAR_Altitude;
|
|||||||
double RADAR_Longitude = 0;
|
double RADAR_Longitude = 0;
|
||||||
double RADAR_Latitude = 0;
|
double RADAR_Latitude = 0;
|
||||||
|
|
||||||
extern uint8_t GUI_start_flag_received;
|
extern uint8_t GUI_start_flag_received; // [STM32-006] Legacy, unused -- kept for linker compat
|
||||||
|
|
||||||
|
|
||||||
//RADAR
|
//RADAR
|
||||||
@@ -620,7 +621,8 @@ typedef enum {
|
|||||||
ERROR_POWER_SUPPLY,
|
ERROR_POWER_SUPPLY,
|
||||||
ERROR_TEMPERATURE_HIGH,
|
ERROR_TEMPERATURE_HIGH,
|
||||||
ERROR_MEMORY_ALLOC,
|
ERROR_MEMORY_ALLOC,
|
||||||
ERROR_WATCHDOG_TIMEOUT
|
ERROR_WATCHDOG_TIMEOUT,
|
||||||
|
ERROR_COUNT // must be last — used for bounds checking error_strings[]
|
||||||
} SystemError_t;
|
} SystemError_t;
|
||||||
|
|
||||||
static SystemError_t last_error = ERROR_NONE;
|
static SystemError_t last_error = ERROR_NONE;
|
||||||
@@ -631,21 +633,42 @@ static bool system_emergency_state = false;
|
|||||||
SystemError_t checkSystemHealth(void) {
|
SystemError_t checkSystemHealth(void) {
|
||||||
SystemError_t current_error = ERROR_NONE;
|
SystemError_t current_error = ERROR_NONE;
|
||||||
|
|
||||||
// 1. Check AD9523 Clock Generator
|
// 0. Watchdog: detect main-loop stall (checkSystemHealth not called for >60 s).
|
||||||
static uint32_t last_clock_check = 0;
|
// Timestamp is captured at function ENTRY and updated unconditionally, so
|
||||||
if (HAL_GetTick() - last_clock_check > 5000) {
|
// any early return from a sub-check below cannot leave a stale value that
|
||||||
GPIO_PinState s0 = HAL_GPIO_ReadPin(AD9523_STATUS0_GPIO_Port, AD9523_STATUS0_Pin);
|
// would later trip a spurious ERROR_WATCHDOG_TIMEOUT. A dedicated cold-start
|
||||||
GPIO_PinState s1 = HAL_GPIO_ReadPin(AD9523_STATUS1_GPIO_Port, AD9523_STATUS1_Pin);
|
// branch ensures the first call after boot never trips (last_health_check==0
|
||||||
DIAG_GPIO("CLK", "AD9523 STATUS0", s0);
|
// would otherwise make `HAL_GetTick() - 0 > 60000` true forever after the
|
||||||
DIAG_GPIO("CLK", "AD9523 STATUS1", s1);
|
// 60-s mark of the init sequence).
|
||||||
if (s0 == GPIO_PIN_RESET || s1 == GPIO_PIN_RESET) {
|
static uint32_t last_health_check = 0;
|
||||||
current_error = ERROR_AD9523_CLOCK;
|
uint32_t now_tick = HAL_GetTick();
|
||||||
DIAG_ERR("CLK", "AD9523 clock health check FAILED (STATUS0=%d STATUS1=%d)", s0, s1);
|
if (last_health_check == 0) {
|
||||||
|
last_health_check = now_tick; // cold start: seed only
|
||||||
|
} else {
|
||||||
|
uint32_t elapsed = now_tick - last_health_check;
|
||||||
|
last_health_check = now_tick; // update BEFORE any early return
|
||||||
|
if (elapsed > 60000) {
|
||||||
|
current_error = ERROR_WATCHDOG_TIMEOUT;
|
||||||
|
DIAG_ERR("SYS", "Health check: Watchdog timeout (>60s since last check)");
|
||||||
return current_error;
|
return current_error;
|
||||||
}
|
}
|
||||||
last_clock_check = HAL_GetTick();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1. Check AD9523 Clock Generator
|
||||||
|
static uint32_t last_clock_check = 0;
|
||||||
|
if (HAL_GetTick() - last_clock_check > 5000) {
|
||||||
|
GPIO_PinState s0 = HAL_GPIO_ReadPin(AD9523_STATUS0_GPIO_Port, AD9523_STATUS0_Pin);
|
||||||
|
GPIO_PinState s1 = HAL_GPIO_ReadPin(AD9523_STATUS1_GPIO_Port, AD9523_STATUS1_Pin);
|
||||||
|
DIAG_GPIO("CLK", "AD9523 STATUS0", s0);
|
||||||
|
DIAG_GPIO("CLK", "AD9523 STATUS1", s1);
|
||||||
|
if (s0 == GPIO_PIN_RESET || s1 == GPIO_PIN_RESET) {
|
||||||
|
current_error = ERROR_AD9523_CLOCK;
|
||||||
|
DIAG_ERR("CLK", "AD9523 clock health check FAILED (STATUS0=%d STATUS1=%d)", s0, s1);
|
||||||
|
return current_error;
|
||||||
|
}
|
||||||
|
last_clock_check = HAL_GetTick();
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Check ADF4382 Lock Status
|
// 2. Check ADF4382 Lock Status
|
||||||
bool tx_locked, rx_locked;
|
bool tx_locked, rx_locked;
|
||||||
if (ADF4382A_CheckLockStatus(&lo_manager, &tx_locked, &rx_locked) == ADF4382A_MANAGER_OK) {
|
if (ADF4382A_CheckLockStatus(&lo_manager, &tx_locked, &rx_locked) == ADF4382A_MANAGER_OK) {
|
||||||
@@ -679,37 +702,34 @@ SystemError_t checkSystemHealth(void) {
|
|||||||
|
|
||||||
// 4. Check IMU Communication
|
// 4. Check IMU Communication
|
||||||
static uint32_t last_imu_check = 0;
|
static uint32_t last_imu_check = 0;
|
||||||
if (HAL_GetTick() - last_imu_check > 10000) {
|
if (HAL_GetTick() - last_imu_check > 10000) {
|
||||||
if (!GY85_Update(&imu)) {
|
if (!GY85_Update(&imu)) {
|
||||||
current_error = ERROR_IMU_COMM;
|
current_error = ERROR_IMU_COMM;
|
||||||
DIAG_ERR("IMU", "Health check: GY85_Update() FAILED");
|
DIAG_ERR("IMU", "Health check: GY85_Update() FAILED");
|
||||||
return current_error;
|
return current_error;
|
||||||
}
|
}
|
||||||
last_imu_check = HAL_GetTick();
|
last_imu_check = HAL_GetTick();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Check BMP180 Communication
|
// 5. Check BMP180 Communication
|
||||||
static uint32_t last_bmp_check = 0;
|
static uint32_t last_bmp_check = 0;
|
||||||
if (HAL_GetTick() - last_bmp_check > 15000) {
|
if (HAL_GetTick() - last_bmp_check > 15000) {
|
||||||
double pressure = myBMP.getPressure();
|
double pressure = myBMP.getPressure();
|
||||||
if (pressure < 30000.0 || pressure > 110000.0 || isnan(pressure)) {
|
if (pressure < 30000.0 || pressure > 110000.0 || isnan(pressure)) {
|
||||||
current_error = ERROR_BMP180_COMM;
|
current_error = ERROR_BMP180_COMM;
|
||||||
DIAG_ERR("SYS", "Health check: BMP180 pressure out of range: %.0f", pressure);
|
DIAG_ERR("SYS", "Health check: BMP180 pressure out of range: %.0f", pressure);
|
||||||
return current_error;
|
return current_error;
|
||||||
}
|
}
|
||||||
last_bmp_check = HAL_GetTick();
|
last_bmp_check = HAL_GetTick();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Check GPS Communication
|
// 6. Check GPS Communication (30s grace period from boot / last valid fix)
|
||||||
static uint32_t last_gps_fix = 0;
|
uint32_t gps_fix_age = um982_position_age(&um982);
|
||||||
if (gps.location.isUpdated()) {
|
if (gps_fix_age > 30000) {
|
||||||
last_gps_fix = HAL_GetTick();
|
current_error = ERROR_GPS_COMM;
|
||||||
}
|
DIAG_WARN("SYS", "Health check: GPS no fix for >30s (age=%lu ms)", (unsigned long)gps_fix_age);
|
||||||
if (HAL_GetTick() - last_gps_fix > 30000) {
|
return current_error;
|
||||||
current_error = ERROR_GPS_COMM;
|
}
|
||||||
DIAG_WARN("SYS", "Health check: GPS no fix for >30s");
|
|
||||||
return current_error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. Check RF Power Amplifier Current
|
// 7. Check RF Power Amplifier Current
|
||||||
if (PowerAmplifier) {
|
if (PowerAmplifier) {
|
||||||
@@ -734,14 +754,7 @@ SystemError_t checkSystemHealth(void) {
|
|||||||
return current_error;
|
return current_error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. Simple watchdog check
|
// 9. Watchdog check is performed at function entry (see step 0).
|
||||||
static uint32_t last_health_check = 0;
|
|
||||||
if (HAL_GetTick() - last_health_check > 60000) {
|
|
||||||
current_error = ERROR_WATCHDOG_TIMEOUT;
|
|
||||||
DIAG_ERR("SYS", "Health check: Watchdog timeout (>60s since last check)");
|
|
||||||
return current_error;
|
|
||||||
}
|
|
||||||
last_health_check = HAL_GetTick();
|
|
||||||
|
|
||||||
if (current_error != ERROR_NONE) {
|
if (current_error != ERROR_NONE) {
|
||||||
DIAG_ERR("SYS", "checkSystemHealth returning error code %d", current_error);
|
DIAG_ERR("SYS", "checkSystemHealth returning error code %d", current_error);
|
||||||
@@ -853,7 +866,7 @@ void handleSystemError(SystemError_t error) {
|
|||||||
DIAG_ERR("SYS", "handleSystemError: error=%d error_count=%lu", error, error_count);
|
DIAG_ERR("SYS", "handleSystemError: error=%d error_count=%lu", error, error_count);
|
||||||
|
|
||||||
char error_msg[100];
|
char error_msg[100];
|
||||||
const char* error_strings[] = {
|
static const char* const error_strings[] = {
|
||||||
"No error",
|
"No error",
|
||||||
"AD9523 Clock failure",
|
"AD9523 Clock failure",
|
||||||
"ADF4382 TX LO unlocked",
|
"ADF4382 TX LO unlocked",
|
||||||
@@ -873,9 +886,16 @@ void handleSystemError(SystemError_t error) {
|
|||||||
"Watchdog timeout"
|
"Watchdog timeout"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(error_strings) / sizeof(error_strings[0]) == ERROR_COUNT,
|
||||||
|
"error_strings[] and SystemError_t enum are out of sync");
|
||||||
|
|
||||||
|
const char* err_name = (error >= 0 && error < (int)(sizeof(error_strings) / sizeof(error_strings[0])))
|
||||||
|
? error_strings[error]
|
||||||
|
: "Unknown error";
|
||||||
|
|
||||||
snprintf(error_msg, sizeof(error_msg),
|
snprintf(error_msg, sizeof(error_msg),
|
||||||
"ERROR #%d: %s (Count: %lu)\r\n",
|
"ERROR #%d: %s (Count: %lu)\r\n",
|
||||||
error, error_strings[error], error_count);
|
error, err_name, error_count);
|
||||||
HAL_UART_Transmit(&huart3, (uint8_t*)error_msg, strlen(error_msg), 1000);
|
HAL_UART_Transmit(&huart3, (uint8_t*)error_msg, strlen(error_msg), 1000);
|
||||||
|
|
||||||
// Blink LED pattern based on error code
|
// Blink LED pattern based on error code
|
||||||
@@ -885,9 +905,23 @@ void handleSystemError(SystemError_t error) {
|
|||||||
HAL_Delay(200);
|
HAL_Delay(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Critical errors trigger emergency shutdown
|
// Critical errors trigger emergency shutdown.
|
||||||
if (error >= ERROR_RF_PA_OVERCURRENT && error <= ERROR_POWER_SUPPLY) {
|
//
|
||||||
DIAG_ERR("SYS", "CRITICAL ERROR (code %d: %s) -- initiating Emergency_Stop()", error, error_strings[error]);
|
// Safety-critical range: any fault that can damage the PAs or leave the
|
||||||
|
// system in an undefined state must cut the RF rails via Emergency_Stop().
|
||||||
|
// This covers:
|
||||||
|
// ERROR_RF_PA_OVERCURRENT .. ERROR_POWER_SUPPLY (9..13) -- PA/supply faults
|
||||||
|
// ERROR_TEMPERATURE_HIGH (14) -- >75 C on the PA thermal sensors;
|
||||||
|
// without cutting bias + 5V/5V5/RFPA rails
|
||||||
|
// the GaN QPA2962 stage can thermal-runaway.
|
||||||
|
// ERROR_WATCHDOG_TIMEOUT (16) -- health-check loop has stalled (>60 s);
|
||||||
|
// transmitter state is unknown, safest to
|
||||||
|
// latch Emergency_Stop rather than rely on
|
||||||
|
// IWDG reset (which re-energises the rails).
|
||||||
|
if ((error >= ERROR_RF_PA_OVERCURRENT && error <= ERROR_POWER_SUPPLY) ||
|
||||||
|
error == ERROR_TEMPERATURE_HIGH ||
|
||||||
|
error == ERROR_WATCHDOG_TIMEOUT) {
|
||||||
|
DIAG_ERR("SYS", "CRITICAL ERROR (code %d: %s) -- initiating Emergency_Stop()", error, err_name);
|
||||||
snprintf(error_msg, sizeof(error_msg),
|
snprintf(error_msg, sizeof(error_msg),
|
||||||
"CRITICAL ERROR! Initiating emergency shutdown.\r\n");
|
"CRITICAL ERROR! Initiating emergency shutdown.\r\n");
|
||||||
HAL_UART_Transmit(&huart3, (uint8_t*)error_msg, strlen(error_msg), 1000);
|
HAL_UART_Transmit(&huart3, (uint8_t*)error_msg, strlen(error_msg), 1000);
|
||||||
@@ -1020,20 +1054,7 @@ static inline void delay_ms(uint32_t ms) { HAL_Delay(ms); }
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// This custom version of delay() ensures that the gps object
|
// smartDelay removed -- replaced by non-blocking um982_process() in main loop
|
||||||
// is being "fed".
|
|
||||||
static void smartDelay(unsigned long ms)
|
|
||||||
{
|
|
||||||
uint32_t start = HAL_GetTick();
|
|
||||||
uint8_t ch;
|
|
||||||
|
|
||||||
do {
|
|
||||||
// While there is new data available in UART (non-blocking)
|
|
||||||
if (HAL_UART_Receive(&huart5, &ch, 1, 0) == HAL_OK) {
|
|
||||||
gps.encode(ch); // Pass received byte to TinyGPS++ equivalent parser
|
|
||||||
}
|
|
||||||
} while (HAL_GetTick() - start < ms);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Small helper to enable DWT cycle counter for microdelay
|
// Small helper to enable DWT cycle counter for microdelay
|
||||||
static void DWT_Init(void)
|
static void DWT_Init(void)
|
||||||
@@ -1177,7 +1198,14 @@ static int configure_ad9523(void)
|
|||||||
|
|
||||||
// init ad9523 defaults (fills any missing pdata defaults)
|
// init ad9523 defaults (fills any missing pdata defaults)
|
||||||
DIAG("CLK", "Calling ad9523_init() -- fills pdata defaults");
|
DIAG("CLK", "Calling ad9523_init() -- fills pdata defaults");
|
||||||
ad9523_init(&init_param);
|
{
|
||||||
|
int32_t init_ret = ad9523_init(&init_param);
|
||||||
|
DIAG("CLK", "ad9523_init() returned %ld", (long)init_ret);
|
||||||
|
if (init_ret != 0) {
|
||||||
|
DIAG_ERR("CLK", "ad9523_init() FAILED (ret=%ld)", (long)init_ret);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* [Bug #2 FIXED] Removed first ad9523_setup() call that was here.
|
/* [Bug #2 FIXED] Removed first ad9523_setup() call that was here.
|
||||||
* It wrote to the chip while still in reset — writes were lost.
|
* It wrote to the chip while still in reset — writes were lost.
|
||||||
@@ -1566,6 +1594,12 @@ int main(void)
|
|||||||
Yaw_Sensor = (180*atan2(magRawY,magRawX)/PI) - Mag_Declination;
|
Yaw_Sensor = (180*atan2(magRawY,magRawX)/PI) - Mag_Declination;
|
||||||
|
|
||||||
if(Yaw_Sensor<0)Yaw_Sensor+=360;
|
if(Yaw_Sensor<0)Yaw_Sensor+=360;
|
||||||
|
|
||||||
|
// Override magnetometer heading with UM982 dual-antenna heading when available
|
||||||
|
if (um982_is_heading_valid(&um982)) {
|
||||||
|
Yaw_Sensor = um982_get_heading(&um982);
|
||||||
|
}
|
||||||
|
|
||||||
RxEst_0 = RxEst_1;
|
RxEst_0 = RxEst_1;
|
||||||
RyEst_0 = RyEst_1;
|
RyEst_0 = RyEst_1;
|
||||||
RzEst_0 = RzEst_1;
|
RzEst_0 = RzEst_1;
|
||||||
@@ -1741,14 +1775,38 @@ int main(void)
|
|||||||
//////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////
|
||||||
//////////////////////////////////////////GPS/////////////////////////////////////////
|
//////////////////////////////////////////GPS/////////////////////////////////////////
|
||||||
//////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////
|
||||||
for(int i=0; i<10;i++){
|
DIAG_SECTION("GPS INIT (UM982)");
|
||||||
smartDelay(1000);
|
DIAG("GPS", "Initializing UM982 on UART5 @ 115200 (baseline=50cm, tol=3cm)");
|
||||||
RADAR_Longitude = gps.location.lng();
|
if (!um982_init(&um982, &huart5, 50.0f, 3.0f)) {
|
||||||
RADAR_Latitude = gps.location.lat();
|
DIAG_WARN("GPS", "UM982 init: no VERSIONA response -- module may need more time");
|
||||||
|
// Not fatal: module may still start sending NMEA data after boot
|
||||||
|
} else {
|
||||||
|
DIAG("GPS", "UM982 init OK -- VERSIONA received");
|
||||||
}
|
}
|
||||||
|
|
||||||
//move Stepper to position 1 = 0°
|
// Collect GPS data for a few seconds (non-blocking pump)
|
||||||
HAL_GPIO_WritePin(STEPPER_CW_P_GPIO_Port, STEPPER_CW_P_Pin, GPIO_PIN_RESET);//Set stepper motor spinning direction to CCW
|
DIAG("GPS", "Pumping GPS for 5 seconds to acquire initial fix...");
|
||||||
|
{
|
||||||
|
uint32_t gps_start = HAL_GetTick();
|
||||||
|
while (HAL_GetTick() - gps_start < 5000) {
|
||||||
|
um982_process(&um982);
|
||||||
|
HAL_Delay(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RADAR_Longitude = um982_get_longitude(&um982);
|
||||||
|
RADAR_Latitude = um982_get_latitude(&um982);
|
||||||
|
DIAG("GPS", "Initial position: lat=%.6f lon=%.6f fix=%d sats=%d",
|
||||||
|
RADAR_Latitude, RADAR_Longitude,
|
||||||
|
um982_get_fix_quality(&um982), um982_get_num_sats(&um982));
|
||||||
|
|
||||||
|
// Re-apply heading after GPS init so the north-alignment stepper move uses
|
||||||
|
// UM982 dual-antenna heading when available.
|
||||||
|
if (um982_is_heading_valid(&um982)) {
|
||||||
|
Yaw_Sensor = um982_get_heading(&um982);
|
||||||
|
}
|
||||||
|
|
||||||
|
//move Stepper to position 1 = 0°
|
||||||
|
HAL_GPIO_WritePin(STEPPER_CW_P_GPIO_Port, STEPPER_CW_P_Pin, GPIO_PIN_RESET);//Set stepper motor spinning direction to CCW
|
||||||
//Point Stepper to North
|
//Point Stepper to North
|
||||||
for(int i= 0;i<(int)(Yaw_Sensor*Stepper_steps/360);i++){
|
for(int i= 0;i<(int)(Yaw_Sensor*Stepper_steps/360);i++){
|
||||||
HAL_GPIO_WritePin(STEPPER_CLK_P_GPIO_Port, STEPPER_CLK_P_Pin, GPIO_PIN_SET);
|
HAL_GPIO_WritePin(STEPPER_CLK_P_GPIO_Port, STEPPER_CLK_P_Pin, GPIO_PIN_SET);
|
||||||
@@ -1770,29 +1828,11 @@ int main(void)
|
|||||||
HAL_UART_Transmit(&huart3, (uint8_t*)gps_send_error, sizeof(gps_send_error) - 1, 1000);
|
HAL_UART_Transmit(&huart3, (uint8_t*)gps_send_error, sizeof(gps_send_error) - 1, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if start flag was received and settings are ready
|
/* [STM32-006 FIXED] Removed blocking do-while loop that waited for
|
||||||
do{
|
* usbHandler.isStartFlagReceived(). The production V7 PyQt GUI does not
|
||||||
if (usbHandler.isStartFlagReceived() &&
|
* send the legacy 4-byte start flag [23,46,158,237], so this loop hung
|
||||||
usbHandler.getState() == USBHandler::USBState::READY_FOR_DATA) {
|
* the MCU at boot indefinitely. The USB settings handshake (if ever
|
||||||
|
* re-enabled) should be handled non-blocking in the main loop. */
|
||||||
const RadarSettings& settings = usbHandler.getSettings();
|
|
||||||
|
|
||||||
// Use the settings to configure your radar system
|
|
||||||
/*
|
|
||||||
settings.getSystemFrequency();
|
|
||||||
settings.getChirpDuration1();
|
|
||||||
settings.getChirpDuration2();
|
|
||||||
settings.getChirpsPerPosition();
|
|
||||||
settings.getFreqMin();
|
|
||||||
settings.getFreqMax();
|
|
||||||
settings.getPRF1();
|
|
||||||
settings.getPRF2();
|
|
||||||
settings.getMaxDistance();
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}while(!usbHandler.isStartFlagReceived());
|
|
||||||
|
|
||||||
/***************************************************************/
|
/***************************************************************/
|
||||||
/************RF Power Amplifier Powering up sequence************/
|
/************RF Power Amplifier Powering up sequence************/
|
||||||
@@ -2017,6 +2057,18 @@ int main(void)
|
|||||||
}
|
}
|
||||||
DIAG("SYS", "Exited safe mode blink loop -- system_emergency_state cleared");
|
DIAG("SYS", "Exited safe mode blink loop -- system_emergency_state cleared");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
////////////////////////// GPS: Non-blocking NMEA processing ////////////////////////
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
um982_process(&um982);
|
||||||
|
|
||||||
|
// Update position globals continuously
|
||||||
|
if (um982_is_position_valid(&um982)) {
|
||||||
|
RADAR_Latitude = um982_get_latitude(&um982);
|
||||||
|
RADAR_Longitude = um982_get_longitude(&um982);
|
||||||
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////
|
||||||
////////////////////////// Monitor ADF4382A lock status periodically//////////////////
|
////////////////////////// Monitor ADF4382A lock status periodically//////////////////
|
||||||
//////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -2127,9 +2179,24 @@ int main(void)
|
|||||||
|
|
||||||
runRadarPulseSequence();
|
runRadarPulseSequence();
|
||||||
|
|
||||||
/* [AGC] Outer-loop AGC: read FPGA saturation flag (DIG_5 / PD13),
|
/* [AGC] Outer-loop AGC: sync enable from FPGA via DIG_6 (PD14),
|
||||||
* adjust ADAR1000 VGA common gain once per radar frame (~258 ms).
|
* then read saturation flag (DIG_5 / PD13) and adjust ADAR1000 VGA
|
||||||
* Only run when AGC is enabled — otherwise leave VGA gains untouched. */
|
* common gain once per radar frame (~258 ms).
|
||||||
|
* FPGA register host_agc_enable is the single source of truth —
|
||||||
|
* DIG_6 propagates it to MCU every frame.
|
||||||
|
* 2-frame confirmation debounce: only change outerAgc.enabled when
|
||||||
|
* two consecutive frames read the same DIG_6 value. Prevents a
|
||||||
|
* single-sample glitch from causing a spurious AGC state transition.
|
||||||
|
* Added latency: 1 extra frame (~258 ms), acceptable for control plane. */
|
||||||
|
{
|
||||||
|
bool dig6_now = (HAL_GPIO_ReadPin(FPGA_DIG6_GPIO_Port,
|
||||||
|
FPGA_DIG6_Pin) == GPIO_PIN_SET);
|
||||||
|
static bool dig6_prev = false; // matches boot default (AGC off)
|
||||||
|
if (dig6_now == dig6_prev) {
|
||||||
|
outerAgc.enabled = dig6_now;
|
||||||
|
}
|
||||||
|
dig6_prev = dig6_now;
|
||||||
|
}
|
||||||
if (outerAgc.enabled) {
|
if (outerAgc.enabled) {
|
||||||
bool sat = HAL_GPIO_ReadPin(FPGA_DIG5_SAT_GPIO_Port,
|
bool sat = HAL_GPIO_ReadPin(FPGA_DIG5_SAT_GPIO_Port,
|
||||||
FPGA_DIG5_SAT_Pin) == GPIO_PIN_SET;
|
FPGA_DIG5_SAT_Pin) == GPIO_PIN_SET;
|
||||||
@@ -2567,7 +2634,7 @@ static void MX_UART5_Init(void)
|
|||||||
|
|
||||||
/* USER CODE END UART5_Init 1 */
|
/* USER CODE END UART5_Init 1 */
|
||||||
huart5.Instance = UART5;
|
huart5.Instance = UART5;
|
||||||
huart5.Init.BaudRate = 9600;
|
huart5.Init.BaudRate = 115200;
|
||||||
huart5.Init.WordLength = UART_WORDLENGTH_8B;
|
huart5.Init.WordLength = UART_WORDLENGTH_8B;
|
||||||
huart5.Init.StopBits = UART_STOPBITS_1;
|
huart5.Init.StopBits = UART_STOPBITS_1;
|
||||||
huart5.Init.Parity = UART_PARITY_NONE;
|
huart5.Init.Parity = UART_PARITY_NONE;
|
||||||
|
|||||||
@@ -0,0 +1,586 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* um982_gps.c -- UM982 dual-antenna GNSS receiver driver implementation
|
||||||
|
*
|
||||||
|
* See um982_gps.h for API documentation.
|
||||||
|
* Command syntax per Unicore N4 Command Reference EN R1.14.
|
||||||
|
******************************************************************************/
|
||||||
|
#include "um982_gps.h"
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
/* ========================= Internal helpers ========================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advance to the next comma-delimited field in an NMEA sentence.
|
||||||
|
* Returns pointer to the start of the next field (after the comma),
|
||||||
|
* or NULL if no more commas found before end-of-string or '*'.
|
||||||
|
*
|
||||||
|
* Handles empty fields (consecutive commas) correctly by returning
|
||||||
|
* a pointer to the character after the comma (which may be another comma).
|
||||||
|
*/
|
||||||
|
static const char *next_field(const char *p)
|
||||||
|
{
|
||||||
|
if (p == NULL) return NULL;
|
||||||
|
while (*p != '\0' && *p != ',' && *p != '*') {
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
if (*p == ',') return p + 1;
|
||||||
|
return NULL; /* End of sentence or checksum marker */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the length of the current field (up to next comma, '*', or '\0').
|
||||||
|
*/
|
||||||
|
static int field_len(const char *p)
|
||||||
|
{
|
||||||
|
int len = 0;
|
||||||
|
if (p == NULL) return 0;
|
||||||
|
while (p[len] != '\0' && p[len] != ',' && p[len] != '*') {
|
||||||
|
len++;
|
||||||
|
}
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a field is non-empty (has at least one character before delimiter).
|
||||||
|
*/
|
||||||
|
static bool field_valid(const char *p)
|
||||||
|
{
|
||||||
|
return p != NULL && field_len(p) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a floating-point value from a field, returning 0.0 if empty.
|
||||||
|
*/
|
||||||
|
static double field_to_double(const char *p)
|
||||||
|
{
|
||||||
|
if (!field_valid(p)) return 0.0;
|
||||||
|
return strtod(p, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static float field_to_float(const char *p)
|
||||||
|
{
|
||||||
|
return (float)field_to_double(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int field_to_int(const char *p)
|
||||||
|
{
|
||||||
|
if (!field_valid(p)) return 0;
|
||||||
|
return (int)strtol(p, NULL, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================= Checksum ================================== */
|
||||||
|
|
||||||
|
bool um982_verify_checksum(const char *sentence)
|
||||||
|
{
|
||||||
|
if (sentence == NULL || sentence[0] != '$') return false;
|
||||||
|
|
||||||
|
const char *p = sentence + 1; /* Skip '$' */
|
||||||
|
uint8_t computed = 0;
|
||||||
|
|
||||||
|
while (*p != '\0' && *p != '*') {
|
||||||
|
computed ^= (uint8_t)*p;
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*p != '*') return false; /* No checksum marker found */
|
||||||
|
p++; /* Skip '*' */
|
||||||
|
|
||||||
|
/* Parse 2-char hex checksum */
|
||||||
|
if (p[0] == '\0' || p[1] == '\0') return false;
|
||||||
|
|
||||||
|
char hex_str[3] = { p[0], p[1], '\0' };
|
||||||
|
unsigned long expected = strtoul(hex_str, NULL, 16);
|
||||||
|
|
||||||
|
return computed == (uint8_t)expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================= Coordinate parsing ======================== */
|
||||||
|
|
||||||
|
double um982_parse_coord(const char *field, char hemisphere)
|
||||||
|
{
|
||||||
|
if (field == NULL || field[0] == '\0') return NAN;
|
||||||
|
|
||||||
|
/* Find the decimal point to determine degree digit count.
|
||||||
|
* Latitude: ddmm.mmmm (dot at index 4, degrees = 2)
|
||||||
|
* Longitude: dddmm.mmmm (dot at index 5, degrees = 3)
|
||||||
|
* General: degree_digits = dot_position - 2
|
||||||
|
*/
|
||||||
|
const char *dot = strchr(field, '.');
|
||||||
|
if (dot == NULL) return NAN;
|
||||||
|
|
||||||
|
int dot_pos = (int)(dot - field);
|
||||||
|
int deg_digits = dot_pos - 2;
|
||||||
|
|
||||||
|
if (deg_digits < 1 || deg_digits > 3) return NAN;
|
||||||
|
|
||||||
|
/* Extract degree portion */
|
||||||
|
double degrees = 0.0;
|
||||||
|
for (int i = 0; i < deg_digits; i++) {
|
||||||
|
if (field[i] < '0' || field[i] > '9') return NAN;
|
||||||
|
degrees = degrees * 10.0 + (field[i] - '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extract minutes portion (everything from deg_digits onward) */
|
||||||
|
double minutes = strtod(field + deg_digits, NULL);
|
||||||
|
if (minutes < 0.0 || minutes >= 60.0) return NAN;
|
||||||
|
|
||||||
|
double result = degrees + minutes / 60.0;
|
||||||
|
|
||||||
|
/* Apply hemisphere sign */
|
||||||
|
if (hemisphere == 'S' || hemisphere == 'W') {
|
||||||
|
result = -result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================= Sentence parsers ========================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identify the NMEA sentence type by skipping the 2-char talker ID
|
||||||
|
* and comparing the 3-letter formatter.
|
||||||
|
*
|
||||||
|
* "$GNGGA,..." -> talker="GN", formatter="GGA"
|
||||||
|
* "$GPTHS,..." -> talker="GP", formatter="THS"
|
||||||
|
*
|
||||||
|
* Returns pointer to the formatter (3 chars at sentence+3), or NULL
|
||||||
|
* if sentence is too short.
|
||||||
|
*/
|
||||||
|
static const char *get_formatter(const char *sentence)
|
||||||
|
{
|
||||||
|
/* sentence starts with '$', followed by 2-char talker + 3-char formatter */
|
||||||
|
if (sentence == NULL || strlen(sentence) < 6) return NULL;
|
||||||
|
return sentence + 3; /* Skip "$XX" -> points to formatter */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse GGA sentence — position and fix quality.
|
||||||
|
*
|
||||||
|
* Format: $--GGA,time,lat,N/S,lon,E/W,quality,numSat,hdop,alt,M,geoidSep,M,dgpsAge,refID*XX
|
||||||
|
* field: 1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
||||||
|
*/
|
||||||
|
static void parse_gga(UM982_GPS_t *gps, const char *sentence)
|
||||||
|
{
|
||||||
|
/* Skip to first field (after "$XXGGA,") */
|
||||||
|
const char *f = strchr(sentence, ',');
|
||||||
|
if (f == NULL) return;
|
||||||
|
f++; /* f -> field 1 (time) */
|
||||||
|
|
||||||
|
/* Field 1: UTC time — skip for now */
|
||||||
|
const char *f2 = next_field(f); /* lat */
|
||||||
|
const char *f3 = next_field(f2); /* N/S */
|
||||||
|
const char *f4 = next_field(f3); /* lon */
|
||||||
|
const char *f5 = next_field(f4); /* E/W */
|
||||||
|
const char *f6 = next_field(f5); /* quality */
|
||||||
|
const char *f7 = next_field(f6); /* numSat */
|
||||||
|
const char *f8 = next_field(f7); /* hdop */
|
||||||
|
const char *f9 = next_field(f8); /* altitude */
|
||||||
|
const char *f10 = next_field(f9); /* M */
|
||||||
|
const char *f11 = next_field(f10); /* geoid sep */
|
||||||
|
|
||||||
|
uint32_t now = HAL_GetTick();
|
||||||
|
|
||||||
|
/* Parse fix quality first — if 0, position is meaningless */
|
||||||
|
gps->fix_quality = (uint8_t)field_to_int(f6);
|
||||||
|
|
||||||
|
/* Parse coordinates */
|
||||||
|
if (field_valid(f2) && field_valid(f3)) {
|
||||||
|
char hem = field_valid(f3) ? *f3 : 'N';
|
||||||
|
double lat = um982_parse_coord(f2, hem);
|
||||||
|
if (!isnan(lat)) gps->latitude = lat;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field_valid(f4) && field_valid(f5)) {
|
||||||
|
char hem = field_valid(f5) ? *f5 : 'E';
|
||||||
|
double lon = um982_parse_coord(f4, hem);
|
||||||
|
if (!isnan(lon)) gps->longitude = lon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Number of satellites */
|
||||||
|
gps->num_satellites = (uint8_t)field_to_int(f7);
|
||||||
|
|
||||||
|
/* HDOP */
|
||||||
|
if (field_valid(f8)) {
|
||||||
|
gps->hdop = field_to_float(f8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Altitude */
|
||||||
|
if (field_valid(f9)) {
|
||||||
|
gps->altitude = field_to_float(f9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Geoid separation */
|
||||||
|
if (field_valid(f11)) {
|
||||||
|
gps->geoid_sep = field_to_float(f11);
|
||||||
|
}
|
||||||
|
|
||||||
|
gps->last_gga_tick = now;
|
||||||
|
if (gps->fix_quality != UM982_FIX_NONE) {
|
||||||
|
gps->last_fix_tick = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse RMC sentence — recommended minimum (position, speed, date).
|
||||||
|
*
|
||||||
|
* Format: $--RMC,time,status,lat,N/S,lon,E/W,speed,course,date,magVar,E/W,mode*XX
|
||||||
|
* field: 1 2 3 4 5 6 7 8 9 10 11 12
|
||||||
|
*/
|
||||||
|
static void parse_rmc(UM982_GPS_t *gps, const char *sentence)
|
||||||
|
{
|
||||||
|
const char *f = strchr(sentence, ',');
|
||||||
|
if (f == NULL) return;
|
||||||
|
f++; /* f -> field 1 (time) */
|
||||||
|
|
||||||
|
const char *f2 = next_field(f); /* status */
|
||||||
|
const char *f3 = next_field(f2); /* lat */
|
||||||
|
const char *f4 = next_field(f3); /* N/S */
|
||||||
|
const char *f5 = next_field(f4); /* lon */
|
||||||
|
const char *f6 = next_field(f5); /* E/W */
|
||||||
|
const char *f7 = next_field(f6); /* speed knots */
|
||||||
|
const char *f8 = next_field(f7); /* course true */
|
||||||
|
|
||||||
|
/* Status */
|
||||||
|
if (field_valid(f2)) {
|
||||||
|
gps->rmc_status = *f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Position (only if status = A for valid) */
|
||||||
|
if (field_valid(f2) && *f2 == 'A') {
|
||||||
|
if (field_valid(f3) && field_valid(f4)) {
|
||||||
|
double lat = um982_parse_coord(f3, *f4);
|
||||||
|
if (!isnan(lat)) gps->latitude = lat;
|
||||||
|
}
|
||||||
|
if (field_valid(f5) && field_valid(f6)) {
|
||||||
|
double lon = um982_parse_coord(f5, *f6);
|
||||||
|
if (!isnan(lon)) gps->longitude = lon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Speed (knots) */
|
||||||
|
if (field_valid(f7)) {
|
||||||
|
gps->speed_knots = field_to_float(f7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Course */
|
||||||
|
if (field_valid(f8)) {
|
||||||
|
gps->course_true = field_to_float(f8);
|
||||||
|
}
|
||||||
|
|
||||||
|
gps->last_rmc_tick = HAL_GetTick();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse THS sentence — true heading and status (UM982-specific).
|
||||||
|
*
|
||||||
|
* Format: $--THS,heading,mode*XX
|
||||||
|
* field: 1 2
|
||||||
|
*/
|
||||||
|
static void parse_ths(UM982_GPS_t *gps, const char *sentence)
|
||||||
|
{
|
||||||
|
const char *f = strchr(sentence, ',');
|
||||||
|
if (f == NULL) return;
|
||||||
|
f++; /* f -> field 1 (heading) */
|
||||||
|
|
||||||
|
const char *f2 = next_field(f); /* mode */
|
||||||
|
|
||||||
|
/* Heading */
|
||||||
|
if (field_valid(f)) {
|
||||||
|
gps->heading = field_to_float(f);
|
||||||
|
} else {
|
||||||
|
gps->heading = NAN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mode */
|
||||||
|
if (field_valid(f2)) {
|
||||||
|
gps->heading_mode = *f2;
|
||||||
|
} else {
|
||||||
|
gps->heading_mode = 'V'; /* Not valid if missing */
|
||||||
|
}
|
||||||
|
|
||||||
|
gps->last_ths_tick = HAL_GetTick();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse VTG sentence — course and speed over ground.
|
||||||
|
*
|
||||||
|
* Format: $--VTG,courseTrue,T,courseMag,M,speedKnots,N,speedKmh,K,mode*XX
|
||||||
|
* field: 1 2 3 4 5 6 7 8 9
|
||||||
|
*/
|
||||||
|
static void parse_vtg(UM982_GPS_t *gps, const char *sentence)
|
||||||
|
{
|
||||||
|
const char *f = strchr(sentence, ',');
|
||||||
|
if (f == NULL) return;
|
||||||
|
f++; /* f -> field 1 (course true) */
|
||||||
|
|
||||||
|
const char *f2 = next_field(f); /* T */
|
||||||
|
const char *f3 = next_field(f2); /* course mag */
|
||||||
|
const char *f4 = next_field(f3); /* M */
|
||||||
|
const char *f5 = next_field(f4); /* speed knots */
|
||||||
|
const char *f6 = next_field(f5); /* N */
|
||||||
|
const char *f7 = next_field(f6); /* speed km/h */
|
||||||
|
|
||||||
|
/* Course true */
|
||||||
|
if (field_valid(f)) {
|
||||||
|
gps->course_true = field_to_float(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Speed knots */
|
||||||
|
if (field_valid(f5)) {
|
||||||
|
gps->speed_knots = field_to_float(f5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Speed km/h */
|
||||||
|
if (field_valid(f7)) {
|
||||||
|
gps->speed_kmh = field_to_float(f7);
|
||||||
|
}
|
||||||
|
|
||||||
|
gps->last_vtg_tick = HAL_GetTick();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================= Sentence dispatch ========================= */
|
||||||
|
|
||||||
|
void um982_parse_sentence(UM982_GPS_t *gps, const char *sentence)
|
||||||
|
{
|
||||||
|
if (sentence == NULL || sentence[0] != '$') return;
|
||||||
|
|
||||||
|
/* Verify checksum before parsing */
|
||||||
|
if (!um982_verify_checksum(sentence)) return;
|
||||||
|
|
||||||
|
/* Check for VERSIONA response (starts with '#', not '$') -- handled separately */
|
||||||
|
/* Actually VERSIONA starts with '#', so it won't enter here. We check in feed(). */
|
||||||
|
|
||||||
|
/* Identify sentence type */
|
||||||
|
const char *fmt = get_formatter(sentence);
|
||||||
|
if (fmt == NULL) return;
|
||||||
|
|
||||||
|
if (strncmp(fmt, "GGA", 3) == 0) {
|
||||||
|
gps->initialized = true;
|
||||||
|
parse_gga(gps, sentence);
|
||||||
|
} else if (strncmp(fmt, "RMC", 3) == 0) {
|
||||||
|
gps->initialized = true;
|
||||||
|
parse_rmc(gps, sentence);
|
||||||
|
} else if (strncmp(fmt, "THS", 3) == 0) {
|
||||||
|
gps->initialized = true;
|
||||||
|
parse_ths(gps, sentence);
|
||||||
|
} else if (strncmp(fmt, "VTG", 3) == 0) {
|
||||||
|
gps->initialized = true;
|
||||||
|
parse_vtg(gps, sentence);
|
||||||
|
}
|
||||||
|
/* Other sentences silently ignored */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================= Command interface ========================= */
|
||||||
|
|
||||||
|
bool um982_send_command(UM982_GPS_t *gps, const char *cmd)
|
||||||
|
{
|
||||||
|
if (gps == NULL || gps->huart == NULL || cmd == NULL) return false;
|
||||||
|
|
||||||
|
/* Build command with \r\n termination */
|
||||||
|
char buf[UM982_CMD_BUF_SIZE];
|
||||||
|
int len = snprintf(buf, sizeof(buf), "%s\r\n", cmd);
|
||||||
|
if (len <= 0 || (size_t)len >= sizeof(buf)) return false;
|
||||||
|
|
||||||
|
HAL_StatusTypeDef status = HAL_UART_Transmit(
|
||||||
|
gps->huart, (const uint8_t *)buf, (uint16_t)len, 100);
|
||||||
|
|
||||||
|
return status == HAL_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================= Line assembly + feed ====================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a completed line from the line buffer.
|
||||||
|
*/
|
||||||
|
static void process_line(UM982_GPS_t *gps, const char *line)
|
||||||
|
{
|
||||||
|
if (line == NULL || line[0] == '\0') return;
|
||||||
|
|
||||||
|
/* NMEA sentence starts with '$' */
|
||||||
|
if (line[0] == '$') {
|
||||||
|
um982_parse_sentence(gps, line);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Unicore proprietary response starts with '#' (e.g. #VERSIONA) */
|
||||||
|
if (line[0] == '#') {
|
||||||
|
if (strncmp(line + 1, "VERSIONA", 8) == 0) {
|
||||||
|
gps->version_received = true;
|
||||||
|
gps->initialized = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void um982_feed(UM982_GPS_t *gps, const uint8_t *data, uint16_t len)
|
||||||
|
{
|
||||||
|
if (gps == NULL || data == NULL || len == 0) return;
|
||||||
|
|
||||||
|
for (uint16_t i = 0; i < len; i++) {
|
||||||
|
uint8_t ch = data[i];
|
||||||
|
|
||||||
|
/* End of line: process if we have content */
|
||||||
|
if (ch == '\n' || ch == '\r') {
|
||||||
|
if (gps->line_len > 0 && !gps->line_overflow) {
|
||||||
|
gps->line_buf[gps->line_len] = '\0';
|
||||||
|
process_line(gps, gps->line_buf);
|
||||||
|
}
|
||||||
|
gps->line_len = 0;
|
||||||
|
gps->line_overflow = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Accumulate into line buffer */
|
||||||
|
if (gps->line_len < UM982_LINE_BUF_SIZE - 1) {
|
||||||
|
gps->line_buf[gps->line_len++] = (char)ch;
|
||||||
|
} else {
|
||||||
|
gps->line_overflow = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================= UART process (production) ================= */
|
||||||
|
|
||||||
|
void um982_process(UM982_GPS_t *gps)
|
||||||
|
{
|
||||||
|
if (gps == NULL || gps->huart == NULL) return;
|
||||||
|
|
||||||
|
/* Read all available bytes from the UART one at a time.
|
||||||
|
* At 115200 baud (~11.5 KB/s) and a typical main-loop period of ~10 ms,
|
||||||
|
* we expect ~115 bytes per call — negligible overhead on a 168 MHz STM32.
|
||||||
|
*
|
||||||
|
* Note: batch reads (HAL_UART_Receive with Size > 1 and Timeout = 0) are
|
||||||
|
* NOT safe here because the HAL consumes bytes from the data register as
|
||||||
|
* it reads them. If fewer than Size bytes are available, the consumed
|
||||||
|
* bytes are lost (HAL_TIMEOUT is returned and the caller has no way to
|
||||||
|
* know how many bytes were actually placed into the buffer). */
|
||||||
|
uint8_t ch;
|
||||||
|
while (HAL_UART_Receive(gps->huart, &ch, 1, 0) == HAL_OK) {
|
||||||
|
um982_feed(gps, &ch, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================= Validity checks =========================== */
|
||||||
|
|
||||||
|
bool um982_is_heading_valid(const UM982_GPS_t *gps)
|
||||||
|
{
|
||||||
|
if (gps == NULL) return false;
|
||||||
|
if (isnan(gps->heading)) return false;
|
||||||
|
|
||||||
|
/* Mode must be Autonomous or Differential */
|
||||||
|
if (gps->heading_mode != 'A' && gps->heading_mode != 'D') return false;
|
||||||
|
|
||||||
|
/* Check age */
|
||||||
|
uint32_t age = HAL_GetTick() - gps->last_ths_tick;
|
||||||
|
return age < UM982_HEADING_TIMEOUT_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool um982_is_position_valid(const UM982_GPS_t *gps)
|
||||||
|
{
|
||||||
|
if (gps == NULL) return false;
|
||||||
|
if (gps->fix_quality == UM982_FIX_NONE) return false;
|
||||||
|
|
||||||
|
/* Check age of the last valid fix */
|
||||||
|
uint32_t age = HAL_GetTick() - gps->last_fix_tick;
|
||||||
|
return age < UM982_POSITION_TIMEOUT_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t um982_heading_age(const UM982_GPS_t *gps)
|
||||||
|
{
|
||||||
|
if (gps == NULL) return UINT32_MAX;
|
||||||
|
return HAL_GetTick() - gps->last_ths_tick;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t um982_position_age(const UM982_GPS_t *gps)
|
||||||
|
{
|
||||||
|
if (gps == NULL) return UINT32_MAX;
|
||||||
|
return HAL_GetTick() - gps->last_fix_tick;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================= Initialization ============================ */
|
||||||
|
|
||||||
|
bool um982_init(UM982_GPS_t *gps, UART_HandleTypeDef *huart,
|
||||||
|
float baseline_cm, float tolerance_cm)
|
||||||
|
{
|
||||||
|
if (gps == NULL || huart == NULL) return false;
|
||||||
|
|
||||||
|
/* Zero-init entire structure */
|
||||||
|
memset(gps, 0, sizeof(UM982_GPS_t));
|
||||||
|
|
||||||
|
gps->huart = huart;
|
||||||
|
gps->heading = NAN;
|
||||||
|
gps->heading_mode = 'V';
|
||||||
|
gps->rmc_status = 'V';
|
||||||
|
gps->speed_knots = 0.0f;
|
||||||
|
|
||||||
|
/* Seed fix timestamp so position_age() returns ~0 instead of uptime.
|
||||||
|
* Gives the module a full 30s grace window from init to acquire a fix
|
||||||
|
* before the health check fires ERROR_GPS_COMM. */
|
||||||
|
gps->last_fix_tick = HAL_GetTick();
|
||||||
|
gps->speed_kmh = 0.0f;
|
||||||
|
gps->course_true = 0.0f;
|
||||||
|
|
||||||
|
/* Step 1: Stop all current output to get a clean slate */
|
||||||
|
um982_send_command(gps, "UNLOG");
|
||||||
|
HAL_Delay(100);
|
||||||
|
|
||||||
|
/* Step 2: Configure heading mode
|
||||||
|
* Per N4 Reference 4.18: CONFIG HEADING FIXLENGTH (default mode)
|
||||||
|
* "The distance between ANT1 and ANT2 is fixed. They move synchronously." */
|
||||||
|
um982_send_command(gps, "CONFIG HEADING FIXLENGTH");
|
||||||
|
HAL_Delay(50);
|
||||||
|
|
||||||
|
/* Step 3: Set baseline length if specified
|
||||||
|
* Per N4 Reference: CONFIG HEADING LENGTH <cm> <tolerance_cm>
|
||||||
|
* "parameter1: Fixed baseline length (cm), valid range >= 0"
|
||||||
|
* "parameter2: Tolerable error margin (cm), valid range > 0" */
|
||||||
|
if (baseline_cm > 0.0f) {
|
||||||
|
char cmd[64];
|
||||||
|
if (tolerance_cm > 0.0f) {
|
||||||
|
snprintf(cmd, sizeof(cmd), "CONFIG HEADING LENGTH %.0f %.0f",
|
||||||
|
baseline_cm, tolerance_cm);
|
||||||
|
} else {
|
||||||
|
snprintf(cmd, sizeof(cmd), "CONFIG HEADING LENGTH %.0f",
|
||||||
|
baseline_cm);
|
||||||
|
}
|
||||||
|
um982_send_command(gps, cmd);
|
||||||
|
HAL_Delay(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Step 4: Enable NMEA output sentences on COM2.
|
||||||
|
* Per N4 Reference: "When requesting NMEA messages, users should add GP
|
||||||
|
* before each command name"
|
||||||
|
*
|
||||||
|
* We target COM2 because the ELT0213 board (GNSS.STORE) exposes COM2
|
||||||
|
* (RXD2/TXD2) on its 12-pin JST connector (pins 5 & 6). The STM32
|
||||||
|
* UART5 (PC12-TX, PD2-RX) connects to these pins via JP8.
|
||||||
|
* COM2 defaults to 115200 baud — matching our UART5 config. */
|
||||||
|
um982_send_command(gps, "GPGGA COM2 1"); /* GGA at 1 Hz */
|
||||||
|
HAL_Delay(50);
|
||||||
|
um982_send_command(gps, "GPRMC COM2 1"); /* RMC at 1 Hz */
|
||||||
|
HAL_Delay(50);
|
||||||
|
um982_send_command(gps, "GPTHS COM2 0.2"); /* THS at 5 Hz (heading primary) */
|
||||||
|
HAL_Delay(50);
|
||||||
|
|
||||||
|
/* Step 5: Skip SAVECONFIG -- NMEA config is re-sent every boot anyway.
|
||||||
|
* Saving to NVM on every power cycle would wear flash. If persistent
|
||||||
|
* config is needed, call um982_send_command(gps, "SAVECONFIG") once
|
||||||
|
* during commissioning. */
|
||||||
|
|
||||||
|
/* Step 6: Query version to verify communication */
|
||||||
|
gps->version_received = false;
|
||||||
|
um982_send_command(gps, "VERSIONA");
|
||||||
|
|
||||||
|
/* Wait for VERSIONA response (non-blocking poll) */
|
||||||
|
uint32_t start = HAL_GetTick();
|
||||||
|
while (!gps->version_received &&
|
||||||
|
(HAL_GetTick() - start) < UM982_INIT_TIMEOUT_MS) {
|
||||||
|
um982_process(gps);
|
||||||
|
HAL_Delay(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
gps->initialized = gps->version_received;
|
||||||
|
return gps->initialized;
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* um982_gps.h -- UM982 dual-antenna GNSS receiver driver
|
||||||
|
*
|
||||||
|
* Parses NMEA sentences (GGA, RMC, THS, VTG) from the Unicore UM982 module
|
||||||
|
* and provides position, heading, and velocity data.
|
||||||
|
*
|
||||||
|
* Design principles:
|
||||||
|
* - Non-blocking: process() reads available UART bytes without waiting
|
||||||
|
* - Correct NMEA parsing: proper tokenizer handles empty fields
|
||||||
|
* - Longitude handles 3-digit degrees (dddmm.mmmm) via decimal-point detection
|
||||||
|
* - Checksum verified on every sentence
|
||||||
|
* - Command syntax verified against Unicore N4 Command Reference EN R1.14
|
||||||
|
*
|
||||||
|
* Hardware: UM982 on UART5 @ 115200 baud, dual-antenna heading mode
|
||||||
|
******************************************************************************/
|
||||||
|
#ifndef UM982_GPS_H
|
||||||
|
#define UM982_GPS_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Forward-declare the HAL UART handle type. The real definition comes from
|
||||||
|
* stm32f7xx_hal.h (production) or stm32_hal_mock.h (tests). */
|
||||||
|
#ifndef STM32_HAL_MOCK_H
|
||||||
|
#include "stm32f7xx_hal.h"
|
||||||
|
#else
|
||||||
|
/* Already included via mock -- nothing to do */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* ========================= Constants ================================= */
|
||||||
|
|
||||||
|
#define UM982_RX_BUF_SIZE 512 /* Ring buffer for incoming UART bytes */
|
||||||
|
#define UM982_LINE_BUF_SIZE 96 /* Max NMEA sentence (82 chars + margin) */
|
||||||
|
#define UM982_CMD_BUF_SIZE 128 /* Outgoing command buffer */
|
||||||
|
#define UM982_INIT_TIMEOUT_MS 3000 /* Timeout waiting for VERSIONA response */
|
||||||
|
|
||||||
|
/* Fix quality values (from GGA field 6) */
|
||||||
|
#define UM982_FIX_NONE 0
|
||||||
|
#define UM982_FIX_GPS 1
|
||||||
|
#define UM982_FIX_DGPS 2
|
||||||
|
#define UM982_FIX_RTK_FIXED 4
|
||||||
|
#define UM982_FIX_RTK_FLOAT 5
|
||||||
|
|
||||||
|
/* Validity timeout defaults (ms) */
|
||||||
|
#define UM982_HEADING_TIMEOUT_MS 2000
|
||||||
|
#define UM982_POSITION_TIMEOUT_MS 5000
|
||||||
|
|
||||||
|
/* ========================= Data Types ================================ */
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
/* Position */
|
||||||
|
double latitude; /* Decimal degrees, positive = North */
|
||||||
|
double longitude; /* Decimal degrees, positive = East */
|
||||||
|
float altitude; /* Meters above MSL */
|
||||||
|
float geoid_sep; /* Geoid separation (meters) */
|
||||||
|
|
||||||
|
/* Heading (from dual-antenna THS) */
|
||||||
|
float heading; /* True heading 0-360 degrees, NAN if invalid */
|
||||||
|
char heading_mode; /* A=autonomous, D=diff, E=est, M=manual, S=sim, V=invalid */
|
||||||
|
|
||||||
|
/* Velocity */
|
||||||
|
float speed_knots; /* Speed over ground (knots) */
|
||||||
|
float speed_kmh; /* Speed over ground (km/h) */
|
||||||
|
float course_true; /* Course over ground (degrees true) */
|
||||||
|
|
||||||
|
/* Quality */
|
||||||
|
uint8_t fix_quality; /* 0=none, 1=GPS, 2=DGPS, 4=RTK fixed, 5=RTK float */
|
||||||
|
uint8_t num_satellites; /* Satellites used in fix */
|
||||||
|
float hdop; /* Horizontal dilution of precision */
|
||||||
|
|
||||||
|
/* RMC status */
|
||||||
|
char rmc_status; /* A=valid, V=warning */
|
||||||
|
|
||||||
|
/* Timestamps (HAL_GetTick() at last update) */
|
||||||
|
uint32_t last_fix_tick; /* Last valid GGA fix (fix_quality > 0) */
|
||||||
|
uint32_t last_gga_tick;
|
||||||
|
uint32_t last_rmc_tick;
|
||||||
|
uint32_t last_ths_tick;
|
||||||
|
uint32_t last_vtg_tick;
|
||||||
|
|
||||||
|
/* Communication state */
|
||||||
|
bool initialized; /* VERSIONA or supported NMEA traffic seen */
|
||||||
|
bool version_received; /* VERSIONA response seen */
|
||||||
|
|
||||||
|
/* ---- Internal parser state (not for external use) ---- */
|
||||||
|
|
||||||
|
/* Ring buffer */
|
||||||
|
uint8_t rx_buf[UM982_RX_BUF_SIZE];
|
||||||
|
uint16_t rx_head; /* Write index */
|
||||||
|
uint16_t rx_tail; /* Read index */
|
||||||
|
|
||||||
|
/* Line assembler */
|
||||||
|
char line_buf[UM982_LINE_BUF_SIZE];
|
||||||
|
uint8_t line_len;
|
||||||
|
bool line_overflow; /* Current line exceeded buffer */
|
||||||
|
|
||||||
|
/* UART handle */
|
||||||
|
UART_HandleTypeDef *huart;
|
||||||
|
|
||||||
|
} UM982_GPS_t;
|
||||||
|
|
||||||
|
/* ========================= Public API ================================ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the UM982_GPS_t structure and configure the module.
|
||||||
|
*
|
||||||
|
* Sends: UNLOG, CONFIG HEADING, optional CONFIG HEADING LENGTH,
|
||||||
|
* GPGGA, GPRMC, GPTHS
|
||||||
|
* Queries VERSIONA to verify communication.
|
||||||
|
*
|
||||||
|
* @param gps Pointer to UM982_GPS_t instance
|
||||||
|
* @param huart UART handle (e.g. &huart5)
|
||||||
|
* @param baseline_cm Distance between antennas in cm (0 = use module default)
|
||||||
|
* @param tolerance_cm Baseline tolerance in cm (0 = use module default)
|
||||||
|
* @return true if VERSIONA response received within timeout
|
||||||
|
*/
|
||||||
|
bool um982_init(UM982_GPS_t *gps, UART_HandleTypeDef *huart,
|
||||||
|
float baseline_cm, float tolerance_cm);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process available UART data. Call from main loop — non-blocking.
|
||||||
|
*
|
||||||
|
* Reads all available bytes from UART, assembles lines, and dispatches
|
||||||
|
* complete NMEA sentences to the appropriate parser.
|
||||||
|
*
|
||||||
|
* @param gps Pointer to UM982_GPS_t instance
|
||||||
|
*/
|
||||||
|
void um982_process(UM982_GPS_t *gps);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feed raw bytes directly into the parser (useful for testing).
|
||||||
|
* In production, um982_process() calls this internally after UART read.
|
||||||
|
*
|
||||||
|
* @param gps Pointer to UM982_GPS_t instance
|
||||||
|
* @param data Pointer to byte array
|
||||||
|
* @param len Number of bytes
|
||||||
|
*/
|
||||||
|
void um982_feed(UM982_GPS_t *gps, const uint8_t *data, uint16_t len);
|
||||||
|
|
||||||
|
/* ---- Getters ---- */
|
||||||
|
|
||||||
|
static inline float um982_get_heading(const UM982_GPS_t *gps) { return gps->heading; }
|
||||||
|
static inline double um982_get_latitude(const UM982_GPS_t *gps) { return gps->latitude; }
|
||||||
|
static inline double um982_get_longitude(const UM982_GPS_t *gps) { return gps->longitude; }
|
||||||
|
static inline float um982_get_altitude(const UM982_GPS_t *gps) { return gps->altitude; }
|
||||||
|
static inline uint8_t um982_get_fix_quality(const UM982_GPS_t *gps) { return gps->fix_quality; }
|
||||||
|
static inline uint8_t um982_get_num_sats(const UM982_GPS_t *gps) { return gps->num_satellites; }
|
||||||
|
static inline float um982_get_hdop(const UM982_GPS_t *gps) { return gps->hdop; }
|
||||||
|
static inline float um982_get_speed_knots(const UM982_GPS_t *gps) { return gps->speed_knots; }
|
||||||
|
static inline float um982_get_speed_kmh(const UM982_GPS_t *gps) { return gps->speed_kmh; }
|
||||||
|
static inline float um982_get_course(const UM982_GPS_t *gps) { return gps->course_true; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if heading is valid (mode A or D, and within timeout).
|
||||||
|
*/
|
||||||
|
bool um982_is_heading_valid(const UM982_GPS_t *gps);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if position is valid (fix_quality > 0, and within timeout).
|
||||||
|
*/
|
||||||
|
bool um982_is_position_valid(const UM982_GPS_t *gps);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get age of last heading update in milliseconds.
|
||||||
|
*/
|
||||||
|
uint32_t um982_heading_age(const UM982_GPS_t *gps);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get age of the last valid position fix in milliseconds.
|
||||||
|
*/
|
||||||
|
uint32_t um982_position_age(const UM982_GPS_t *gps);
|
||||||
|
|
||||||
|
/* ========================= Internal (exposed for testing) ============ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify NMEA checksum. Returns true if valid.
|
||||||
|
* Sentence must start with '$' and contain '*XX' before termination.
|
||||||
|
*/
|
||||||
|
bool um982_verify_checksum(const char *sentence);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a complete NMEA line (with $ prefix and *XX checksum).
|
||||||
|
* Dispatches to GGA/RMC/THS/VTG parsers as appropriate.
|
||||||
|
*/
|
||||||
|
void um982_parse_sentence(UM982_GPS_t *gps, const char *sentence);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse NMEA coordinate string to decimal degrees.
|
||||||
|
* Works for both latitude (ddmm.mmmm) and longitude (dddmm.mmmm)
|
||||||
|
* by detecting the decimal point position.
|
||||||
|
*
|
||||||
|
* @param field NMEA coordinate field (e.g. "4404.14036" or "12118.85961")
|
||||||
|
* @param hemisphere 'N', 'S', 'E', or 'W'
|
||||||
|
* @return Decimal degrees (negative for S/W), or NAN on parse error
|
||||||
|
*/
|
||||||
|
double um982_parse_coord(const char *field, char hemisphere);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a command to the UM982. Appends \r\n automatically.
|
||||||
|
* @return true if UART transmit succeeded
|
||||||
|
*/
|
||||||
|
bool um982_send_command(UM982_GPS_t *gps, const char *cmd);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* UM982_GPS_H */
|
||||||
@@ -3,18 +3,38 @@
|
|||||||
*.dSYM/
|
*.dSYM/
|
||||||
|
|
||||||
# Test binaries (built by Makefile)
|
# Test binaries (built by Makefile)
|
||||||
|
# TESTS_WITH_REAL
|
||||||
test_bug1_timed_sync_init_ordering
|
test_bug1_timed_sync_init_ordering
|
||||||
test_bug2_ad9523_double_setup
|
|
||||||
test_bug3_timed_sync_noop
|
test_bug3_timed_sync_noop
|
||||||
test_bug4_phase_shift_before_check
|
test_bug4_phase_shift_before_check
|
||||||
test_bug5_fine_phase_gpio_only
|
test_bug5_fine_phase_gpio_only
|
||||||
|
test_bug9_platform_ops_null
|
||||||
|
test_bug10_spi_cs_not_toggled
|
||||||
|
test_bug15_htim3_dangling_extern
|
||||||
|
|
||||||
|
# TESTS_MOCK_ONLY
|
||||||
|
test_bug2_ad9523_double_setup
|
||||||
test_bug6_timer_variable_collision
|
test_bug6_timer_variable_collision
|
||||||
test_bug7_gpio_pin_conflict
|
test_bug7_gpio_pin_conflict
|
||||||
test_bug8_uart_commented_out
|
test_bug8_uart_commented_out
|
||||||
test_bug9_platform_ops_null
|
test_bug14_diag_section_args
|
||||||
test_bug10_spi_cs_not_toggled
|
test_gap3_emergency_stop_rails
|
||||||
test_bug11_platform_spi_transmit_only
|
|
||||||
|
# TESTS_STANDALONE
|
||||||
test_bug12_pa_cal_loop_inverted
|
test_bug12_pa_cal_loop_inverted
|
||||||
test_bug13_dac2_adc_buffer_mismatch
|
test_bug13_dac2_adc_buffer_mismatch
|
||||||
test_bug14_diag_section_args
|
test_gap3_iwdg_config
|
||||||
test_bug15_htim3_dangling_extern
|
test_gap3_temperature_max
|
||||||
|
test_gap3_idq_periodic_reread
|
||||||
|
test_gap3_emergency_state_ordering
|
||||||
|
test_gap3_overtemp_emergency_stop
|
||||||
|
test_gap3_health_watchdog_cold_start
|
||||||
|
|
||||||
|
# TESTS_WITH_PLATFORM
|
||||||
|
test_bug11_platform_spi_transmit_only
|
||||||
|
|
||||||
|
# TESTS_WITH_CXX
|
||||||
|
test_agc_outer_loop
|
||||||
|
|
||||||
|
# Manual / one-off test builds
|
||||||
|
test_um982_gps
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ CXX_LIB_DIR := ../9_1_1_C_Cpp_Libraries
|
|||||||
CXX_SRCS := $(CXX_LIB_DIR)/ADAR1000_AGC.cpp $(CXX_LIB_DIR)/ADAR1000_Manager.cpp
|
CXX_SRCS := $(CXX_LIB_DIR)/ADAR1000_AGC.cpp $(CXX_LIB_DIR)/ADAR1000_Manager.cpp
|
||||||
CXX_OBJS := ADAR1000_AGC.o ADAR1000_Manager.o
|
CXX_OBJS := ADAR1000_AGC.o ADAR1000_Manager.o
|
||||||
|
|
||||||
|
# GPS driver source
|
||||||
|
GPS_SRC := ../9_1_3_C_Cpp_Code/um982_gps.c
|
||||||
|
GPS_OBJ := um982_gps.o
|
||||||
|
|
||||||
# Real source files compiled against mock headers
|
# Real source files compiled against mock headers
|
||||||
REAL_SRC := ../9_1_1_C_Cpp_Libraries/adf4382a_manager.c
|
REAL_SRC := ../9_1_1_C_Cpp_Libraries/adf4382a_manager.c
|
||||||
|
|
||||||
@@ -64,7 +68,9 @@ TESTS_STANDALONE := test_bug12_pa_cal_loop_inverted \
|
|||||||
test_gap3_iwdg_config \
|
test_gap3_iwdg_config \
|
||||||
test_gap3_temperature_max \
|
test_gap3_temperature_max \
|
||||||
test_gap3_idq_periodic_reread \
|
test_gap3_idq_periodic_reread \
|
||||||
test_gap3_emergency_state_ordering
|
test_gap3_emergency_state_ordering \
|
||||||
|
test_gap3_overtemp_emergency_stop \
|
||||||
|
test_gap3_health_watchdog_cold_start
|
||||||
|
|
||||||
# Tests that need platform_noos_stm32.o + mocks
|
# Tests that need platform_noos_stm32.o + mocks
|
||||||
TESTS_WITH_PLATFORM := test_bug11_platform_spi_transmit_only
|
TESTS_WITH_PLATFORM := test_bug11_platform_spi_transmit_only
|
||||||
@@ -72,11 +78,15 @@ TESTS_WITH_PLATFORM := test_bug11_platform_spi_transmit_only
|
|||||||
# C++ tests (AGC outer loop)
|
# C++ tests (AGC outer loop)
|
||||||
TESTS_WITH_CXX := test_agc_outer_loop
|
TESTS_WITH_CXX := test_agc_outer_loop
|
||||||
|
|
||||||
ALL_TESTS := $(TESTS_WITH_REAL) $(TESTS_MOCK_ONLY) $(TESTS_STANDALONE) $(TESTS_WITH_PLATFORM) $(TESTS_WITH_CXX)
|
# GPS driver tests (need mocks + GPS source + -lm)
|
||||||
|
TESTS_GPS := test_um982_gps
|
||||||
|
|
||||||
|
ALL_TESTS := $(TESTS_WITH_REAL) $(TESTS_MOCK_ONLY) $(TESTS_STANDALONE) $(TESTS_WITH_PLATFORM) $(TESTS_WITH_CXX) $(TESTS_GPS)
|
||||||
|
|
||||||
.PHONY: all build test clean \
|
.PHONY: all build test clean \
|
||||||
$(addprefix test_,bug1 bug2 bug3 bug4 bug5 bug6 bug7 bug8 bug9 bug10 bug11 bug12 bug13 bug14 bug15) \
|
$(addprefix test_,bug1 bug2 bug3 bug4 bug5 bug6 bug7 bug8 bug9 bug10 bug11 bug12 bug13 bug14 bug15) \
|
||||||
test_gap3_estop test_gap3_iwdg test_gap3_temp test_gap3_idq test_gap3_order
|
test_gap3_estop test_gap3_iwdg test_gap3_temp test_gap3_idq test_gap3_order \
|
||||||
|
test_gap3_overtemp test_gap3_wdog
|
||||||
|
|
||||||
all: build test
|
all: build test
|
||||||
|
|
||||||
@@ -162,6 +172,12 @@ test_gap3_idq_periodic_reread: test_gap3_idq_periodic_reread.c
|
|||||||
test_gap3_emergency_state_ordering: test_gap3_emergency_state_ordering.c
|
test_gap3_emergency_state_ordering: test_gap3_emergency_state_ordering.c
|
||||||
$(CC) $(CFLAGS) $< -o $@
|
$(CC) $(CFLAGS) $< -o $@
|
||||||
|
|
||||||
|
test_gap3_overtemp_emergency_stop: test_gap3_overtemp_emergency_stop.c
|
||||||
|
$(CC) $(CFLAGS) $< -o $@
|
||||||
|
|
||||||
|
test_gap3_health_watchdog_cold_start: test_gap3_health_watchdog_cold_start.c
|
||||||
|
$(CC) $(CFLAGS) $< -o $@
|
||||||
|
|
||||||
# Tests that need platform_noos_stm32.o + mocks
|
# Tests that need platform_noos_stm32.o + mocks
|
||||||
$(TESTS_WITH_PLATFORM): %: %.c $(MOCK_OBJS) $(PLATFORM_OBJ)
|
$(TESTS_WITH_PLATFORM): %: %.c $(MOCK_OBJS) $(PLATFORM_OBJ)
|
||||||
$(CC) $(CFLAGS) $(INCLUDES) $< $(MOCK_OBJS) $(PLATFORM_OBJ) -o $@
|
$(CC) $(CFLAGS) $(INCLUDES) $< $(MOCK_OBJS) $(PLATFORM_OBJ) -o $@
|
||||||
@@ -184,6 +200,20 @@ test_agc_outer_loop: test_agc_outer_loop.cpp $(CXX_OBJS) $(MOCK_OBJS)
|
|||||||
test_agc: test_agc_outer_loop
|
test_agc: test_agc_outer_loop
|
||||||
./test_agc_outer_loop
|
./test_agc_outer_loop
|
||||||
|
|
||||||
|
# --- GPS driver rules ---
|
||||||
|
|
||||||
|
$(GPS_OBJ): $(GPS_SRC)
|
||||||
|
$(CC) $(CFLAGS) $(INCLUDES) -I../9_1_3_C_Cpp_Code -c $< -o $@
|
||||||
|
|
||||||
|
# Note: test includes um982_gps.c directly for white-box testing (static fn access)
|
||||||
|
test_um982_gps: test_um982_gps.c $(MOCK_OBJS)
|
||||||
|
$(CC) $(CFLAGS) $(INCLUDES) -I../9_1_3_C_Cpp_Code $< $(MOCK_OBJS) -lm -o $@
|
||||||
|
|
||||||
|
# Convenience target
|
||||||
|
.PHONY: test_gps
|
||||||
|
test_gps: test_um982_gps
|
||||||
|
./test_um982_gps
|
||||||
|
|
||||||
# --- Individual test targets ---
|
# --- Individual test targets ---
|
||||||
|
|
||||||
test_bug1: test_bug1_timed_sync_init_ordering
|
test_bug1: test_bug1_timed_sync_init_ordering
|
||||||
@@ -246,6 +276,12 @@ test_gap3_idq: test_gap3_idq_periodic_reread
|
|||||||
test_gap3_order: test_gap3_emergency_state_ordering
|
test_gap3_order: test_gap3_emergency_state_ordering
|
||||||
./test_gap3_emergency_state_ordering
|
./test_gap3_emergency_state_ordering
|
||||||
|
|
||||||
|
test_gap3_overtemp: test_gap3_overtemp_emergency_stop
|
||||||
|
./test_gap3_overtemp_emergency_stop
|
||||||
|
|
||||||
|
test_gap3_wdog: test_gap3_health_watchdog_cold_start
|
||||||
|
./test_gap3_health_watchdog_cold_start
|
||||||
|
|
||||||
# --- Clean ---
|
# --- Clean ---
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ SPI_HandleTypeDef hspi4 = { .id = 4 };
|
|||||||
I2C_HandleTypeDef hi2c1 = { .id = 1 };
|
I2C_HandleTypeDef hi2c1 = { .id = 1 };
|
||||||
I2C_HandleTypeDef hi2c2 = { .id = 2 };
|
I2C_HandleTypeDef hi2c2 = { .id = 2 };
|
||||||
UART_HandleTypeDef huart3 = { .id = 3 };
|
UART_HandleTypeDef huart3 = { .id = 3 };
|
||||||
|
UART_HandleTypeDef huart5 = { .id = 5 }; /* GPS UART */
|
||||||
ADC_HandleTypeDef hadc3 = { .id = 3 };
|
ADC_HandleTypeDef hadc3 = { .id = 3 };
|
||||||
TIM_HandleTypeDef htim3 = { .id = 3 };
|
TIM_HandleTypeDef htim3 = { .id = 3 };
|
||||||
|
|
||||||
@@ -34,6 +35,26 @@ uint32_t mock_tick = 0;
|
|||||||
/* ========================= Printf control ========================= */
|
/* ========================= Printf control ========================= */
|
||||||
int mock_printf_enabled = 0;
|
int mock_printf_enabled = 0;
|
||||||
|
|
||||||
|
/* ========================= Mock UART TX capture =================== */
|
||||||
|
uint8_t mock_uart_tx_buf[MOCK_UART_TX_BUF_SIZE];
|
||||||
|
uint16_t mock_uart_tx_len = 0;
|
||||||
|
|
||||||
|
/* ========================= Mock UART RX buffer ==================== */
|
||||||
|
#define MOCK_UART_RX_SLOTS 8
|
||||||
|
|
||||||
|
static struct {
|
||||||
|
uint32_t uart_id;
|
||||||
|
uint8_t buf[MOCK_UART_RX_BUF_SIZE];
|
||||||
|
uint16_t head;
|
||||||
|
uint16_t tail;
|
||||||
|
} mock_uart_rx[MOCK_UART_RX_SLOTS];
|
||||||
|
|
||||||
|
void mock_uart_tx_clear(void)
|
||||||
|
{
|
||||||
|
mock_uart_tx_len = 0;
|
||||||
|
memset(mock_uart_tx_buf, 0, sizeof(mock_uart_tx_buf));
|
||||||
|
}
|
||||||
|
|
||||||
/* ========================= Mock GPIO read ========================= */
|
/* ========================= Mock GPIO read ========================= */
|
||||||
#define GPIO_READ_TABLE_SIZE 32
|
#define GPIO_READ_TABLE_SIZE 32
|
||||||
static struct {
|
static struct {
|
||||||
@@ -49,6 +70,9 @@ void spy_reset(void)
|
|||||||
mock_tick = 0;
|
mock_tick = 0;
|
||||||
mock_printf_enabled = 0;
|
mock_printf_enabled = 0;
|
||||||
memset(gpio_read_table, 0, sizeof(gpio_read_table));
|
memset(gpio_read_table, 0, sizeof(gpio_read_table));
|
||||||
|
memset(mock_uart_rx, 0, sizeof(mock_uart_rx));
|
||||||
|
mock_uart_tx_len = 0;
|
||||||
|
memset(mock_uart_tx_buf, 0, sizeof(mock_uart_tx_buf));
|
||||||
}
|
}
|
||||||
|
|
||||||
const SpyRecord *spy_get(int index)
|
const SpyRecord *spy_get(int index)
|
||||||
@@ -185,6 +209,83 @@ HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pD
|
|||||||
.value = Timeout,
|
.value = Timeout,
|
||||||
.extra = huart
|
.extra = huart
|
||||||
});
|
});
|
||||||
|
/* Capture TX data for test inspection */
|
||||||
|
for (uint16_t i = 0; i < Size && mock_uart_tx_len < MOCK_UART_TX_BUF_SIZE; i++) {
|
||||||
|
mock_uart_tx_buf[mock_uart_tx_len++] = pData[i];
|
||||||
|
}
|
||||||
|
return HAL_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================= Mock UART RX helpers ====================== */
|
||||||
|
|
||||||
|
/* find_rx_slot, mock_uart_rx_load, etc. use the mock_uart_rx declared above */
|
||||||
|
|
||||||
|
static int find_rx_slot(UART_HandleTypeDef *huart)
|
||||||
|
{
|
||||||
|
if (huart == NULL) return -1;
|
||||||
|
/* Find existing slot */
|
||||||
|
for (int i = 0; i < MOCK_UART_RX_SLOTS; i++) {
|
||||||
|
if (mock_uart_rx[i].uart_id == huart->id && mock_uart_rx[i].head != mock_uart_rx[i].tail) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
if (mock_uart_rx[i].uart_id == huart->id) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Find empty slot */
|
||||||
|
for (int i = 0; i < MOCK_UART_RX_SLOTS; i++) {
|
||||||
|
if (mock_uart_rx[i].uart_id == 0) {
|
||||||
|
mock_uart_rx[i].uart_id = huart->id;
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void mock_uart_rx_load(UART_HandleTypeDef *huart, const uint8_t *data, uint16_t len)
|
||||||
|
{
|
||||||
|
int slot = find_rx_slot(huart);
|
||||||
|
if (slot < 0) return;
|
||||||
|
mock_uart_rx[slot].uart_id = huart->id;
|
||||||
|
for (uint16_t i = 0; i < len; i++) {
|
||||||
|
uint16_t next = (mock_uart_rx[slot].head + 1) % MOCK_UART_RX_BUF_SIZE;
|
||||||
|
if (next == mock_uart_rx[slot].tail) break; /* Buffer full */
|
||||||
|
mock_uart_rx[slot].buf[mock_uart_rx[slot].head] = data[i];
|
||||||
|
mock_uart_rx[slot].head = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void mock_uart_rx_clear(UART_HandleTypeDef *huart)
|
||||||
|
{
|
||||||
|
int slot = find_rx_slot(huart);
|
||||||
|
if (slot < 0) return;
|
||||||
|
mock_uart_rx[slot].head = 0;
|
||||||
|
mock_uart_rx[slot].tail = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData,
|
||||||
|
uint16_t Size, uint32_t Timeout)
|
||||||
|
{
|
||||||
|
(void)Timeout;
|
||||||
|
int slot = find_rx_slot(huart);
|
||||||
|
if (slot < 0) return HAL_TIMEOUT;
|
||||||
|
|
||||||
|
for (uint16_t i = 0; i < Size; i++) {
|
||||||
|
if (mock_uart_rx[slot].head == mock_uart_rx[slot].tail) {
|
||||||
|
return HAL_TIMEOUT; /* No more data */
|
||||||
|
}
|
||||||
|
pData[i] = mock_uart_rx[slot].buf[mock_uart_rx[slot].tail];
|
||||||
|
mock_uart_rx[slot].tail = (mock_uart_rx[slot].tail + 1) % MOCK_UART_RX_BUF_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
spy_push((SpyRecord){
|
||||||
|
.type = SPY_UART_RX,
|
||||||
|
.port = NULL,
|
||||||
|
.pin = Size,
|
||||||
|
.value = Timeout,
|
||||||
|
.extra = huart
|
||||||
|
});
|
||||||
|
|
||||||
return HAL_OK;
|
return HAL_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ typedef struct {
|
|||||||
extern SPI_HandleTypeDef hspi1, hspi4;
|
extern SPI_HandleTypeDef hspi1, hspi4;
|
||||||
extern I2C_HandleTypeDef hi2c1, hi2c2;
|
extern I2C_HandleTypeDef hi2c1, hi2c2;
|
||||||
extern UART_HandleTypeDef huart3;
|
extern UART_HandleTypeDef huart3;
|
||||||
|
extern UART_HandleTypeDef huart5; /* GPS UART */
|
||||||
extern ADC_HandleTypeDef hadc3;
|
extern ADC_HandleTypeDef hadc3;
|
||||||
extern TIM_HandleTypeDef htim3; /* Timer for DELADJ PWM */
|
extern TIM_HandleTypeDef htim3; /* Timer for DELADJ PWM */
|
||||||
|
|
||||||
@@ -139,6 +140,7 @@ typedef enum {
|
|||||||
SPY_TIM_SET_COMPARE,
|
SPY_TIM_SET_COMPARE,
|
||||||
SPY_SPI_TRANSMIT_RECEIVE,
|
SPY_SPI_TRANSMIT_RECEIVE,
|
||||||
SPY_SPI_TRANSMIT,
|
SPY_SPI_TRANSMIT,
|
||||||
|
SPY_UART_RX,
|
||||||
} SpyCallType;
|
} SpyCallType;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -187,6 +189,23 @@ void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
|
|||||||
uint32_t HAL_GetTick(void);
|
uint32_t HAL_GetTick(void);
|
||||||
void HAL_Delay(uint32_t Delay);
|
void HAL_Delay(uint32_t Delay);
|
||||||
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout);
|
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout);
|
||||||
|
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
|
||||||
|
|
||||||
|
/* ========================= Mock UART RX buffer ======================= */
|
||||||
|
|
||||||
|
/* Inject bytes into the mock UART RX buffer for a specific UART handle.
|
||||||
|
* HAL_UART_Receive will return these bytes one at a time. */
|
||||||
|
#define MOCK_UART_RX_BUF_SIZE 2048
|
||||||
|
|
||||||
|
void mock_uart_rx_load(UART_HandleTypeDef *huart, const uint8_t *data, uint16_t len);
|
||||||
|
void mock_uart_rx_clear(UART_HandleTypeDef *huart);
|
||||||
|
|
||||||
|
/* Capture buffer for UART TX data (to verify commands sent to GPS module) */
|
||||||
|
#define MOCK_UART_TX_BUF_SIZE 2048
|
||||||
|
|
||||||
|
extern uint8_t mock_uart_tx_buf[MOCK_UART_TX_BUF_SIZE];
|
||||||
|
extern uint16_t mock_uart_tx_len;
|
||||||
|
void mock_uart_tx_clear(void);
|
||||||
|
|
||||||
/* ========================= SPI stubs ============================== */
|
/* ========================= SPI stubs ============================== */
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ static void test_defaults()
|
|||||||
assert(agc.min_gain == 0);
|
assert(agc.min_gain == 0);
|
||||||
assert(agc.max_gain == 127);
|
assert(agc.max_gain == 127);
|
||||||
assert(agc.holdoff_frames == 4);
|
assert(agc.holdoff_frames == 4);
|
||||||
assert(agc.enabled == true);
|
assert(agc.enabled == false); // disabled by default — FPGA DIG_6 is source of truth
|
||||||
assert(agc.holdoff_counter == 0);
|
assert(agc.holdoff_counter == 0);
|
||||||
assert(agc.last_saturated == false);
|
assert(agc.last_saturated == false);
|
||||||
assert(agc.saturation_event_count == 0);
|
assert(agc.saturation_event_count == 0);
|
||||||
@@ -67,6 +67,7 @@ static void test_defaults()
|
|||||||
static void test_saturation_reduces_gain()
|
static void test_saturation_reduces_gain()
|
||||||
{
|
{
|
||||||
ADAR1000_AGC agc;
|
ADAR1000_AGC agc;
|
||||||
|
agc.enabled = true; // default is OFF; enable for this test
|
||||||
uint8_t initial = agc.agc_base_gain; // 30
|
uint8_t initial = agc.agc_base_gain; // 30
|
||||||
|
|
||||||
agc.update(true); // saturation
|
agc.update(true); // saturation
|
||||||
@@ -82,6 +83,7 @@ static void test_saturation_reduces_gain()
|
|||||||
static void test_holdoff_prevents_early_gain_up()
|
static void test_holdoff_prevents_early_gain_up()
|
||||||
{
|
{
|
||||||
ADAR1000_AGC agc;
|
ADAR1000_AGC agc;
|
||||||
|
agc.enabled = true; // default is OFF; enable for this test
|
||||||
agc.update(true); // saturate once -> gain = 26
|
agc.update(true); // saturate once -> gain = 26
|
||||||
uint8_t after_sat = agc.agc_base_gain;
|
uint8_t after_sat = agc.agc_base_gain;
|
||||||
|
|
||||||
@@ -101,6 +103,7 @@ static void test_holdoff_prevents_early_gain_up()
|
|||||||
static void test_recovery_after_holdoff()
|
static void test_recovery_after_holdoff()
|
||||||
{
|
{
|
||||||
ADAR1000_AGC agc;
|
ADAR1000_AGC agc;
|
||||||
|
agc.enabled = true; // default is OFF; enable for this test
|
||||||
agc.update(true); // saturate -> gain = 26
|
agc.update(true); // saturate -> gain = 26
|
||||||
uint8_t after_sat = agc.agc_base_gain;
|
uint8_t after_sat = agc.agc_base_gain;
|
||||||
|
|
||||||
@@ -119,6 +122,7 @@ static void test_recovery_after_holdoff()
|
|||||||
static void test_min_gain_clamp()
|
static void test_min_gain_clamp()
|
||||||
{
|
{
|
||||||
ADAR1000_AGC agc;
|
ADAR1000_AGC agc;
|
||||||
|
agc.enabled = true; // default is OFF; enable for this test
|
||||||
agc.min_gain = 10;
|
agc.min_gain = 10;
|
||||||
agc.agc_base_gain = 12;
|
agc.agc_base_gain = 12;
|
||||||
agc.gain_step_down = 4;
|
agc.gain_step_down = 4;
|
||||||
@@ -136,6 +140,7 @@ static void test_min_gain_clamp()
|
|||||||
static void test_max_gain_clamp()
|
static void test_max_gain_clamp()
|
||||||
{
|
{
|
||||||
ADAR1000_AGC agc;
|
ADAR1000_AGC agc;
|
||||||
|
agc.enabled = true; // default is OFF; enable for this test
|
||||||
agc.max_gain = 32;
|
agc.max_gain = 32;
|
||||||
agc.agc_base_gain = 31;
|
agc.agc_base_gain = 31;
|
||||||
agc.gain_step_up = 2;
|
agc.gain_step_up = 2;
|
||||||
@@ -226,6 +231,7 @@ static void test_apply_gain_spi()
|
|||||||
static void test_reset_preserves_config()
|
static void test_reset_preserves_config()
|
||||||
{
|
{
|
||||||
ADAR1000_AGC agc;
|
ADAR1000_AGC agc;
|
||||||
|
agc.enabled = true; // default is OFF; enable for this test
|
||||||
agc.agc_base_gain = 42;
|
agc.agc_base_gain = 42;
|
||||||
agc.gain_step_down = 8;
|
agc.gain_step_down = 8;
|
||||||
agc.cal_offset[3] = -5;
|
agc.cal_offset[3] = -5;
|
||||||
@@ -255,6 +261,7 @@ static void test_reset_preserves_config()
|
|||||||
static void test_saturation_counter()
|
static void test_saturation_counter()
|
||||||
{
|
{
|
||||||
ADAR1000_AGC agc;
|
ADAR1000_AGC agc;
|
||||||
|
agc.enabled = true; // default is OFF; enable for this test
|
||||||
|
|
||||||
for (int i = 0; i < 10; ++i) {
|
for (int i = 0; i < 10; ++i) {
|
||||||
agc.update(true);
|
agc.update(true);
|
||||||
@@ -274,6 +281,7 @@ static void test_saturation_counter()
|
|||||||
static void test_mixed_sequence()
|
static void test_mixed_sequence()
|
||||||
{
|
{
|
||||||
ADAR1000_AGC agc;
|
ADAR1000_AGC agc;
|
||||||
|
agc.enabled = true; // default is OFF; enable for this test
|
||||||
agc.agc_base_gain = 30;
|
agc.agc_base_gain = 30;
|
||||||
agc.gain_step_down = 4;
|
agc.gain_step_down = 4;
|
||||||
agc.gain_step_up = 1;
|
agc.gain_step_up = 1;
|
||||||
|
|||||||
@@ -34,22 +34,25 @@ static void Mock_Emergency_Stop(void)
|
|||||||
state_was_true_when_estop_called = system_emergency_state;
|
state_was_true_when_estop_called = system_emergency_state;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Error codes (subset matching main.cpp) */
|
/* Error codes (subset matching main.cpp SystemError_t) */
|
||||||
typedef enum {
|
typedef enum {
|
||||||
ERROR_NONE = 0,
|
ERROR_NONE = 0,
|
||||||
ERROR_RF_PA_OVERCURRENT = 9,
|
ERROR_RF_PA_OVERCURRENT = 9,
|
||||||
ERROR_RF_PA_BIAS = 10,
|
ERROR_RF_PA_BIAS = 10,
|
||||||
ERROR_STEPPER_FAULT = 11,
|
ERROR_STEPPER_MOTOR = 11,
|
||||||
ERROR_FPGA_COMM = 12,
|
ERROR_FPGA_COMM = 12,
|
||||||
ERROR_POWER_SUPPLY = 13,
|
ERROR_POWER_SUPPLY = 13,
|
||||||
ERROR_TEMPERATURE_HIGH = 14,
|
ERROR_TEMPERATURE_HIGH = 14,
|
||||||
|
ERROR_MEMORY_ALLOC = 15,
|
||||||
|
ERROR_WATCHDOG_TIMEOUT = 16,
|
||||||
} SystemError_t;
|
} SystemError_t;
|
||||||
|
|
||||||
/* Extracted critical-error handling logic (post-fix ordering) */
|
/* Extracted critical-error handling logic (matches post-fix main.cpp predicate) */
|
||||||
static void simulate_handleSystemError_critical(SystemError_t error)
|
static void simulate_handleSystemError_critical(SystemError_t error)
|
||||||
{
|
{
|
||||||
/* Only critical errors (PA overcurrent through power supply) trigger e-stop */
|
if ((error >= ERROR_RF_PA_OVERCURRENT && error <= ERROR_POWER_SUPPLY) ||
|
||||||
if (error >= ERROR_RF_PA_OVERCURRENT && error <= ERROR_POWER_SUPPLY) {
|
error == ERROR_TEMPERATURE_HIGH ||
|
||||||
|
error == ERROR_WATCHDOG_TIMEOUT) {
|
||||||
/* FIX 5: set flag BEFORE calling Emergency_Stop */
|
/* FIX 5: set flag BEFORE calling Emergency_Stop */
|
||||||
system_emergency_state = true;
|
system_emergency_state = true;
|
||||||
Mock_Emergency_Stop();
|
Mock_Emergency_Stop();
|
||||||
@@ -93,17 +96,39 @@ int main(void)
|
|||||||
assert(state_was_true_when_estop_called == true);
|
assert(state_was_true_when_estop_called == true);
|
||||||
printf("PASS\n");
|
printf("PASS\n");
|
||||||
|
|
||||||
/* Test 4: Non-critical error → no e-stop, flag stays false */
|
/* Test 4: Overtemp → MUST trigger e-stop (was incorrectly non-critical before fix) */
|
||||||
printf(" Test 4: Non-critical error (no e-stop)... ");
|
printf(" Test 4: Overtemp triggers e-stop... ");
|
||||||
system_emergency_state = false;
|
system_emergency_state = false;
|
||||||
emergency_stop_called = false;
|
emergency_stop_called = false;
|
||||||
|
state_was_true_when_estop_called = false;
|
||||||
simulate_handleSystemError_critical(ERROR_TEMPERATURE_HIGH);
|
simulate_handleSystemError_critical(ERROR_TEMPERATURE_HIGH);
|
||||||
|
assert(emergency_stop_called == true);
|
||||||
|
assert(system_emergency_state == true);
|
||||||
|
assert(state_was_true_when_estop_called == true);
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
/* Test 5: Watchdog timeout → MUST trigger e-stop */
|
||||||
|
printf(" Test 5: Watchdog timeout triggers e-stop... ");
|
||||||
|
system_emergency_state = false;
|
||||||
|
emergency_stop_called = false;
|
||||||
|
state_was_true_when_estop_called = false;
|
||||||
|
simulate_handleSystemError_critical(ERROR_WATCHDOG_TIMEOUT);
|
||||||
|
assert(emergency_stop_called == true);
|
||||||
|
assert(system_emergency_state == true);
|
||||||
|
assert(state_was_true_when_estop_called == true);
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
/* Test 6: Non-critical error (memory alloc) → no e-stop */
|
||||||
|
printf(" Test 6: Non-critical error (no e-stop)... ");
|
||||||
|
system_emergency_state = false;
|
||||||
|
emergency_stop_called = false;
|
||||||
|
simulate_handleSystemError_critical(ERROR_MEMORY_ALLOC);
|
||||||
assert(emergency_stop_called == false);
|
assert(emergency_stop_called == false);
|
||||||
assert(system_emergency_state == false);
|
assert(system_emergency_state == false);
|
||||||
printf("PASS\n");
|
printf("PASS\n");
|
||||||
|
|
||||||
/* Test 5: ERROR_NONE → no e-stop */
|
/* Test 7: ERROR_NONE → no e-stop */
|
||||||
printf(" Test 5: ERROR_NONE (no action)... ");
|
printf(" Test 7: ERROR_NONE (no action)... ");
|
||||||
system_emergency_state = false;
|
system_emergency_state = false;
|
||||||
emergency_stop_called = false;
|
emergency_stop_called = false;
|
||||||
simulate_handleSystemError_critical(ERROR_NONE);
|
simulate_handleSystemError_critical(ERROR_NONE);
|
||||||
@@ -111,6 +136,6 @@ int main(void)
|
|||||||
assert(system_emergency_state == false);
|
assert(system_emergency_state == false);
|
||||||
printf("PASS\n");
|
printf("PASS\n");
|
||||||
|
|
||||||
printf("\n=== Gap-3 Fix 5: ALL TESTS PASSED ===\n\n");
|
printf("\n=== Gap-3 Fix 5: ALL 7 TESTS PASSED ===\n\n");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* test_gap3_health_watchdog_cold_start.c
|
||||||
|
*
|
||||||
|
* Safety bug: checkSystemHealth()'s internal watchdog (step 9, pre-fix) had two
|
||||||
|
* linked defects that, once ERROR_WATCHDOG_TIMEOUT was escalated to
|
||||||
|
* Emergency_Stop() by the overtemp/watchdog PR, would false-latch the radar:
|
||||||
|
*
|
||||||
|
* (1) Cold-start false trip:
|
||||||
|
* static uint32_t last_health_check = 0;
|
||||||
|
* if (HAL_GetTick() - last_health_check > 60000) { ... }
|
||||||
|
* On the very first call, last_health_check == 0, so once the MCU has
|
||||||
|
* been up >60 s (which is typical after the ADAR1000 / AD9523 / ADF4382
|
||||||
|
* init sequence) the subtraction `now - 0` exceeds 60 000 ms and the
|
||||||
|
* watchdog trips spuriously.
|
||||||
|
*
|
||||||
|
* (2) Stale-timestamp after early returns:
|
||||||
|
* last_health_check = HAL_GetTick(); // at END of function
|
||||||
|
* Every earlier sub-check (IMU, BMP180, GPS, PA Idq, temperature) has an
|
||||||
|
* `if (fault) return current_error;` path that skips the update. After a
|
||||||
|
* cumulative 60 s of transient faults, the next clean call compares
|
||||||
|
* `now` against the long-stale `last_health_check` and trips.
|
||||||
|
*
|
||||||
|
* After fix: Watchdog logic moved to function ENTRY. A dedicated cold-start
|
||||||
|
* branch seeds the timestamp on the first call without checking.
|
||||||
|
* On every subsequent call, the elapsed delta is captured FIRST
|
||||||
|
* and last_health_check is updated BEFORE any sub-check runs, so
|
||||||
|
* early returns no longer leave a stale value.
|
||||||
|
*
|
||||||
|
* Test strategy:
|
||||||
|
* Extract the post-fix watchdog predicate into a standalone function that
|
||||||
|
* takes a simulated HAL_GetTick() value and returns whether the watchdog
|
||||||
|
* should trip. Walk through boot + fault sequences that would have tripped
|
||||||
|
* the pre-fix code and assert the post-fix code does NOT trip.
|
||||||
|
******************************************************************************/
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
/* --- Post-fix watchdog state + predicate, extracted verbatim --- */
|
||||||
|
static uint32_t last_health_check = 0;
|
||||||
|
|
||||||
|
/* Returns 1 iff this call should raise ERROR_WATCHDOG_TIMEOUT.
|
||||||
|
Updates last_health_check BEFORE returning (matches post-fix behaviour). */
|
||||||
|
static int health_watchdog_step(uint32_t now_tick)
|
||||||
|
{
|
||||||
|
if (last_health_check == 0) {
|
||||||
|
last_health_check = now_tick; /* cold start: seed only, never trip */
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
uint32_t elapsed = now_tick - last_health_check;
|
||||||
|
last_health_check = now_tick; /* update BEFORE any early return */
|
||||||
|
return (elapsed > 60000) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Test helper: reset the static state between scenarios. */
|
||||||
|
static void reset_state(void) { last_health_check = 0; }
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
printf("=== Safety fix: checkSystemHealth() watchdog cold-start + stale-ts ===\n");
|
||||||
|
|
||||||
|
/* ---------- Scenario 1: cold-start after 60 s of init must NOT trip ---- */
|
||||||
|
printf(" Test 1: first call at t=75000 ms (post-init) does not trip... ");
|
||||||
|
reset_state();
|
||||||
|
assert(health_watchdog_step(75000) == 0);
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
/* ---------- Scenario 2: first call far beyond 60 s (PRE-FIX BUG) ------- */
|
||||||
|
printf(" Test 2: first call at t=600000 ms still does not trip... ");
|
||||||
|
reset_state();
|
||||||
|
assert(health_watchdog_step(600000) == 0);
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
/* ---------- Scenario 3: healthy main-loop pacing (10 ms period) -------- */
|
||||||
|
printf(" Test 3: 1000 calls at 10 ms intervals never trip... ");
|
||||||
|
reset_state();
|
||||||
|
(void)health_watchdog_step(1000); /* seed */
|
||||||
|
for (int i = 1; i <= 1000; i++) {
|
||||||
|
assert(health_watchdog_step(1000 + i * 10) == 0);
|
||||||
|
}
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
/* ---------- Scenario 4: stale-timestamp after a burst of early returns -
|
||||||
|
Pre-fix bug: many early returns skipped the timestamp update, so a
|
||||||
|
later clean call would compare `now` against a 60+ s old value. Post-fix,
|
||||||
|
every call (including ones that would have early-returned in the real
|
||||||
|
function) updates the timestamp at the top, so this scenario is modelled
|
||||||
|
by calling health_watchdog_step() on every iteration of the main loop. */
|
||||||
|
printf(" Test 4: 70 s of 100 ms-spaced calls after seed do not trip... ");
|
||||||
|
reset_state();
|
||||||
|
(void)health_watchdog_step(50000); /* seed mid-run */
|
||||||
|
for (int i = 1; i <= 700; i++) { /* 70 s @ 100 ms */
|
||||||
|
int tripped = health_watchdog_step(50000 + i * 100);
|
||||||
|
assert(tripped == 0);
|
||||||
|
}
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
/* ---------- Scenario 5: genuine stall MUST trip ------------------------ */
|
||||||
|
printf(" Test 5: real 60+ s gap between calls does trip... ");
|
||||||
|
reset_state();
|
||||||
|
(void)health_watchdog_step(10000); /* seed */
|
||||||
|
assert(health_watchdog_step(10000 + 60001) == 1);
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
/* ---------- Scenario 6: exactly 60 s gap is the boundary -- do NOT trip
|
||||||
|
Post-fix predicate uses strict >60000, matching the pre-fix comparator. */
|
||||||
|
printf(" Test 6: exactly 60000 ms gap does not trip (boundary)... ");
|
||||||
|
reset_state();
|
||||||
|
(void)health_watchdog_step(10000);
|
||||||
|
assert(health_watchdog_step(10000 + 60000) == 0);
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
/* ---------- Scenario 7: trip, then recover on next paced call ---------- */
|
||||||
|
printf(" Test 7: after a genuine stall+trip, next paced call does not re-trip... ");
|
||||||
|
reset_state();
|
||||||
|
(void)health_watchdog_step(5000); /* seed */
|
||||||
|
assert(health_watchdog_step(5000 + 70000) == 1); /* stall -> trip */
|
||||||
|
assert(health_watchdog_step(5000 + 70000 + 10) == 0); /* resume paced */
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
/* ---------- Scenario 8: HAL_GetTick() 32-bit wrap (~49.7 days) ---------
|
||||||
|
Because we subtract unsigned 32-bit values, wrap is handled correctly as
|
||||||
|
long as the true elapsed time is < 2^32 ms. */
|
||||||
|
printf(" Test 8: tick wrap from 0xFFFFFF00 -> 0x00000064 (200 ms span) does not trip... ");
|
||||||
|
reset_state();
|
||||||
|
(void)health_watchdog_step(0xFFFFFF00u);
|
||||||
|
assert(health_watchdog_step(0x00000064u) == 0); /* elapsed = 0x164 = 356 ms */
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
printf("\n=== Safety fix: ALL TESTS PASSED ===\n\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* test_gap3_overtemp_emergency_stop.c
|
||||||
|
*
|
||||||
|
* Safety bug: handleSystemError() did not escalate ERROR_TEMPERATURE_HIGH
|
||||||
|
* (or ERROR_WATCHDOG_TIMEOUT) to Emergency_Stop().
|
||||||
|
*
|
||||||
|
* Before fix: The critical-error gate was
|
||||||
|
* if (error >= ERROR_RF_PA_OVERCURRENT &&
|
||||||
|
* error <= ERROR_POWER_SUPPLY) { Emergency_Stop(); }
|
||||||
|
* So overtemp (code 14) and watchdog timeout (code 16) fell
|
||||||
|
* through to attemptErrorRecovery()'s default branch (log and
|
||||||
|
* continue), leaving the 10 W GaN PAs biased at >75 °C.
|
||||||
|
*
|
||||||
|
* After fix: The gate also matches ERROR_TEMPERATURE_HIGH and
|
||||||
|
* ERROR_WATCHDOG_TIMEOUT, so thermal and watchdog faults
|
||||||
|
* latch Emergency_Stop() exactly like PA overcurrent.
|
||||||
|
*
|
||||||
|
* Test strategy:
|
||||||
|
* Replicate the critical-error predicate and assert that every error
|
||||||
|
* enum value which threatens RF/power safety is accepted, and that the
|
||||||
|
* non-critical ones (comm, sensor, memory) are not.
|
||||||
|
******************************************************************************/
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
/* Mirror of SystemError_t from main.cpp (keep in lockstep). */
|
||||||
|
typedef enum {
|
||||||
|
ERROR_NONE = 0,
|
||||||
|
ERROR_AD9523_CLOCK,
|
||||||
|
ERROR_ADF4382_TX_UNLOCK,
|
||||||
|
ERROR_ADF4382_RX_UNLOCK,
|
||||||
|
ERROR_ADAR1000_COMM,
|
||||||
|
ERROR_ADAR1000_TEMP,
|
||||||
|
ERROR_IMU_COMM,
|
||||||
|
ERROR_BMP180_COMM,
|
||||||
|
ERROR_GPS_COMM,
|
||||||
|
ERROR_RF_PA_OVERCURRENT,
|
||||||
|
ERROR_RF_PA_BIAS,
|
||||||
|
ERROR_STEPPER_MOTOR,
|
||||||
|
ERROR_FPGA_COMM,
|
||||||
|
ERROR_POWER_SUPPLY,
|
||||||
|
ERROR_TEMPERATURE_HIGH,
|
||||||
|
ERROR_MEMORY_ALLOC,
|
||||||
|
ERROR_WATCHDOG_TIMEOUT
|
||||||
|
} SystemError_t;
|
||||||
|
|
||||||
|
/* Extracted post-fix predicate: returns 1 when Emergency_Stop() must fire. */
|
||||||
|
static int triggers_emergency_stop(SystemError_t e)
|
||||||
|
{
|
||||||
|
return ((e >= ERROR_RF_PA_OVERCURRENT && e <= ERROR_POWER_SUPPLY) ||
|
||||||
|
e == ERROR_TEMPERATURE_HIGH ||
|
||||||
|
e == ERROR_WATCHDOG_TIMEOUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
printf("=== Safety fix: overtemp / watchdog -> Emergency_Stop() ===\n");
|
||||||
|
|
||||||
|
/* --- Errors that MUST latch Emergency_Stop --- */
|
||||||
|
printf(" Test 1: ERROR_RF_PA_OVERCURRENT triggers... ");
|
||||||
|
assert(triggers_emergency_stop(ERROR_RF_PA_OVERCURRENT));
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
printf(" Test 2: ERROR_RF_PA_BIAS triggers... ");
|
||||||
|
assert(triggers_emergency_stop(ERROR_RF_PA_BIAS));
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
printf(" Test 3: ERROR_STEPPER_MOTOR triggers... ");
|
||||||
|
assert(triggers_emergency_stop(ERROR_STEPPER_MOTOR));
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
printf(" Test 4: ERROR_FPGA_COMM triggers... ");
|
||||||
|
assert(triggers_emergency_stop(ERROR_FPGA_COMM));
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
printf(" Test 5: ERROR_POWER_SUPPLY triggers... ");
|
||||||
|
assert(triggers_emergency_stop(ERROR_POWER_SUPPLY));
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
printf(" Test 6: ERROR_TEMPERATURE_HIGH triggers (regression)... ");
|
||||||
|
assert(triggers_emergency_stop(ERROR_TEMPERATURE_HIGH));
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
printf(" Test 7: ERROR_WATCHDOG_TIMEOUT triggers (regression)... ");
|
||||||
|
assert(triggers_emergency_stop(ERROR_WATCHDOG_TIMEOUT));
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
/* --- Errors that MUST NOT escalate (recoverable / informational) --- */
|
||||||
|
printf(" Test 8: ERROR_NONE does not trigger... ");
|
||||||
|
assert(!triggers_emergency_stop(ERROR_NONE));
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
printf(" Test 9: ERROR_AD9523_CLOCK does not trigger... ");
|
||||||
|
assert(!triggers_emergency_stop(ERROR_AD9523_CLOCK));
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
printf(" Test 10: ERROR_ADF4382_TX_UNLOCK does not trigger (recoverable)... ");
|
||||||
|
assert(!triggers_emergency_stop(ERROR_ADF4382_TX_UNLOCK));
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
printf(" Test 11: ERROR_ADAR1000_COMM does not trigger... ");
|
||||||
|
assert(!triggers_emergency_stop(ERROR_ADAR1000_COMM));
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
printf(" Test 12: ERROR_IMU_COMM does not trigger... ");
|
||||||
|
assert(!triggers_emergency_stop(ERROR_IMU_COMM));
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
printf(" Test 13: ERROR_GPS_COMM does not trigger... ");
|
||||||
|
assert(!triggers_emergency_stop(ERROR_GPS_COMM));
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
printf(" Test 14: ERROR_MEMORY_ALLOC does not trigger... ");
|
||||||
|
assert(!triggers_emergency_stop(ERROR_MEMORY_ALLOC));
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
printf("\n=== Safety fix: ALL TESTS PASSED ===\n\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,853 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* test_um982_gps.c -- Unit tests for UM982 GPS driver
|
||||||
|
*
|
||||||
|
* Tests NMEA parsing, checksum validation, coordinate parsing, init sequence,
|
||||||
|
* and validity tracking. Uses the mock HAL infrastructure for UART.
|
||||||
|
*
|
||||||
|
* Build: see Makefile target test_um982_gps
|
||||||
|
* Run: ./test_um982_gps
|
||||||
|
******************************************************************************/
|
||||||
|
#include "stm32_hal_mock.h"
|
||||||
|
#include "../9_1_3_C_Cpp_Code/um982_gps.h"
|
||||||
|
#include "../9_1_3_C_Cpp_Code/um982_gps.c" /* Include .c directly for white-box testing */
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <assert.h>
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
/* ========================= Test helpers ============================== */
|
||||||
|
|
||||||
|
static int tests_passed = 0;
|
||||||
|
static int tests_failed = 0;
|
||||||
|
|
||||||
|
#define TEST(name) \
|
||||||
|
do { printf(" [TEST] %-55s ", name); } while(0)
|
||||||
|
|
||||||
|
#define PASS() \
|
||||||
|
do { printf("PASS\n"); tests_passed++; } while(0)
|
||||||
|
|
||||||
|
#define FAIL(msg) \
|
||||||
|
do { printf("FAIL: %s\n", msg); tests_failed++; } while(0)
|
||||||
|
|
||||||
|
#define ASSERT_TRUE(expr, msg) \
|
||||||
|
do { if (!(expr)) { FAIL(msg); return; } } while(0)
|
||||||
|
|
||||||
|
#define ASSERT_FALSE(expr, msg) \
|
||||||
|
do { if (expr) { FAIL(msg); return; } } while(0)
|
||||||
|
|
||||||
|
#define ASSERT_EQ_INT(a, b, msg) \
|
||||||
|
do { if ((a) != (b)) { \
|
||||||
|
char _buf[256]; \
|
||||||
|
snprintf(_buf, sizeof(_buf), "%s (got %d, expected %d)", msg, (int)(a), (int)(b)); \
|
||||||
|
FAIL(_buf); return; \
|
||||||
|
} } while(0)
|
||||||
|
|
||||||
|
#define ASSERT_NEAR(a, b, tol, msg) \
|
||||||
|
do { if (fabs((double)(a) - (double)(b)) > (tol)) { \
|
||||||
|
char _buf[256]; \
|
||||||
|
snprintf(_buf, sizeof(_buf), "%s (got %.8f, expected %.8f)", msg, (double)(a), (double)(b)); \
|
||||||
|
FAIL(_buf); return; \
|
||||||
|
} } while(0)
|
||||||
|
|
||||||
|
#define ASSERT_NAN(val, msg) \
|
||||||
|
do { if (!isnan(val)) { FAIL(msg); return; } } while(0)
|
||||||
|
|
||||||
|
static UM982_GPS_t gps;
|
||||||
|
|
||||||
|
static void reset_gps(void)
|
||||||
|
{
|
||||||
|
spy_reset();
|
||||||
|
memset(&gps, 0, sizeof(gps));
|
||||||
|
gps.huart = &huart5;
|
||||||
|
gps.heading = NAN;
|
||||||
|
gps.heading_mode = 'V';
|
||||||
|
gps.rmc_status = 'V';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================= Checksum tests ============================ */
|
||||||
|
|
||||||
|
static void test_checksum_valid(void)
|
||||||
|
{
|
||||||
|
TEST("checksum: valid GGA");
|
||||||
|
ASSERT_TRUE(um982_verify_checksum(
|
||||||
|
"$GNGGA,001043.00,4404.14036,N,12118.85961,W,1,12,0.98,1113.0,M,-21.3,M*47"),
|
||||||
|
"should be valid");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_checksum_valid_ths(void)
|
||||||
|
{
|
||||||
|
TEST("checksum: valid THS");
|
||||||
|
ASSERT_TRUE(um982_verify_checksum("$GNTHS,341.3344,A*1F"),
|
||||||
|
"should be valid");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_checksum_invalid(void)
|
||||||
|
{
|
||||||
|
TEST("checksum: invalid (wrong value)");
|
||||||
|
ASSERT_FALSE(um982_verify_checksum("$GNTHS,341.3344,A*FF"),
|
||||||
|
"should be invalid");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_checksum_missing_star(void)
|
||||||
|
{
|
||||||
|
TEST("checksum: missing * marker");
|
||||||
|
ASSERT_FALSE(um982_verify_checksum("$GNTHS,341.3344,A"),
|
||||||
|
"should be invalid");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_checksum_null(void)
|
||||||
|
{
|
||||||
|
TEST("checksum: NULL input");
|
||||||
|
ASSERT_FALSE(um982_verify_checksum(NULL), "should be false");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_checksum_no_dollar(void)
|
||||||
|
{
|
||||||
|
TEST("checksum: missing $ prefix");
|
||||||
|
ASSERT_FALSE(um982_verify_checksum("GNTHS,341.3344,A*1F"),
|
||||||
|
"should be invalid without $");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================= Coordinate parsing tests ================== */
|
||||||
|
|
||||||
|
static void test_coord_latitude_north(void)
|
||||||
|
{
|
||||||
|
TEST("coord: latitude 4404.14036 N");
|
||||||
|
double lat = um982_parse_coord("4404.14036", 'N');
|
||||||
|
/* 44 + 04.14036/60 = 44.069006 */
|
||||||
|
ASSERT_NEAR(lat, 44.069006, 0.000001, "latitude");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_coord_latitude_south(void)
|
||||||
|
{
|
||||||
|
TEST("coord: latitude 3358.92500 S (negative)");
|
||||||
|
double lat = um982_parse_coord("3358.92500", 'S');
|
||||||
|
ASSERT_TRUE(lat < 0.0, "should be negative for S");
|
||||||
|
ASSERT_NEAR(lat, -(33.0 + 58.925/60.0), 0.000001, "latitude");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_coord_longitude_3digit(void)
|
||||||
|
{
|
||||||
|
TEST("coord: longitude 12118.85961 W (3-digit degrees)");
|
||||||
|
double lon = um982_parse_coord("12118.85961", 'W');
|
||||||
|
/* 121 + 18.85961/60 = 121.314327 */
|
||||||
|
ASSERT_TRUE(lon < 0.0, "should be negative for W");
|
||||||
|
ASSERT_NEAR(lon, -(121.0 + 18.85961/60.0), 0.000001, "longitude");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_coord_longitude_east(void)
|
||||||
|
{
|
||||||
|
TEST("coord: longitude 11614.19729 E");
|
||||||
|
double lon = um982_parse_coord("11614.19729", 'E');
|
||||||
|
ASSERT_TRUE(lon > 0.0, "should be positive for E");
|
||||||
|
ASSERT_NEAR(lon, 116.0 + 14.19729/60.0, 0.000001, "longitude");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_coord_empty(void)
|
||||||
|
{
|
||||||
|
TEST("coord: empty string returns NAN");
|
||||||
|
ASSERT_NAN(um982_parse_coord("", 'N'), "should be NAN");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_coord_null(void)
|
||||||
|
{
|
||||||
|
TEST("coord: NULL returns NAN");
|
||||||
|
ASSERT_NAN(um982_parse_coord(NULL, 'N'), "should be NAN");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_coord_no_dot(void)
|
||||||
|
{
|
||||||
|
TEST("coord: no decimal point returns NAN");
|
||||||
|
ASSERT_NAN(um982_parse_coord("440414036", 'N'), "should be NAN");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================= GGA parsing tests ========================= */
|
||||||
|
|
||||||
|
static void test_parse_gga_full(void)
|
||||||
|
{
|
||||||
|
TEST("GGA: full sentence with all fields");
|
||||||
|
reset_gps();
|
||||||
|
mock_set_tick(1000);
|
||||||
|
|
||||||
|
um982_parse_sentence(&gps,
|
||||||
|
"$GNGGA,001043.00,4404.14036,N,12118.85961,W,1,12,0.98,1113.0,M,-21.3,M*47");
|
||||||
|
|
||||||
|
ASSERT_NEAR(gps.latitude, 44.069006, 0.0001, "latitude");
|
||||||
|
ASSERT_NEAR(gps.longitude, -(121.0 + 18.85961/60.0), 0.0001, "longitude");
|
||||||
|
ASSERT_EQ_INT(gps.fix_quality, 1, "fix quality");
|
||||||
|
ASSERT_EQ_INT(gps.num_satellites, 12, "num sats");
|
||||||
|
ASSERT_NEAR(gps.hdop, 0.98, 0.01, "hdop");
|
||||||
|
ASSERT_NEAR(gps.altitude, 1113.0, 0.1, "altitude");
|
||||||
|
ASSERT_NEAR(gps.geoid_sep, -21.3, 0.1, "geoid sep");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_parse_gga_rtk_fixed(void)
|
||||||
|
{
|
||||||
|
TEST("GGA: RTK fixed (quality=4)");
|
||||||
|
reset_gps();
|
||||||
|
|
||||||
|
um982_parse_sentence(&gps,
|
||||||
|
"$GNGGA,023634.00,4004.73871635,N,11614.19729418,E,4,28,0.7,61.0988,M,-8.4923,M,,*5D");
|
||||||
|
|
||||||
|
ASSERT_EQ_INT(gps.fix_quality, 4, "RTK fixed");
|
||||||
|
ASSERT_EQ_INT(gps.num_satellites, 28, "num sats");
|
||||||
|
ASSERT_NEAR(gps.latitude, 40.0 + 4.73871635/60.0, 0.0000001, "latitude");
|
||||||
|
ASSERT_NEAR(gps.longitude, 116.0 + 14.19729418/60.0, 0.0000001, "longitude");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_parse_gga_no_fix(void)
|
||||||
|
{
|
||||||
|
TEST("GGA: no fix (quality=0)");
|
||||||
|
reset_gps();
|
||||||
|
|
||||||
|
/* Compute checksum for this sentence */
|
||||||
|
um982_parse_sentence(&gps,
|
||||||
|
"$GNGGA,235959.00,,,,,0,00,99.99,,,,,,*79");
|
||||||
|
|
||||||
|
ASSERT_EQ_INT(gps.fix_quality, 0, "no fix");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================= RMC parsing tests ========================= */
|
||||||
|
|
||||||
|
static void test_parse_rmc_valid(void)
|
||||||
|
{
|
||||||
|
TEST("RMC: valid position and speed");
|
||||||
|
reset_gps();
|
||||||
|
mock_set_tick(2000);
|
||||||
|
|
||||||
|
um982_parse_sentence(&gps,
|
||||||
|
"$GNRMC,001031.00,A,4404.13993,N,12118.86023,W,0.146,,100117,,,A*7B");
|
||||||
|
|
||||||
|
ASSERT_EQ_INT(gps.rmc_status, 'A', "status");
|
||||||
|
ASSERT_NEAR(gps.latitude, 44.0 + 4.13993/60.0, 0.0001, "latitude");
|
||||||
|
ASSERT_NEAR(gps.longitude, -(121.0 + 18.86023/60.0), 0.0001, "longitude");
|
||||||
|
ASSERT_NEAR(gps.speed_knots, 0.146, 0.001, "speed");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_parse_rmc_void(void)
|
||||||
|
{
|
||||||
|
TEST("RMC: void status (no valid fix)");
|
||||||
|
reset_gps();
|
||||||
|
gps.latitude = 12.34; /* Pre-set to check it doesn't get overwritten */
|
||||||
|
|
||||||
|
um982_parse_sentence(&gps,
|
||||||
|
"$GNRMC,235959.00,V,,,,,,,100117,,,N*64");
|
||||||
|
|
||||||
|
ASSERT_EQ_INT(gps.rmc_status, 'V', "void status");
|
||||||
|
ASSERT_NEAR(gps.latitude, 12.34, 0.001, "lat should not change on void");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================= THS parsing tests ========================= */
|
||||||
|
|
||||||
|
static void test_parse_ths_autonomous(void)
|
||||||
|
{
|
||||||
|
TEST("THS: autonomous heading 341.3344");
|
||||||
|
reset_gps();
|
||||||
|
mock_set_tick(3000);
|
||||||
|
|
||||||
|
um982_parse_sentence(&gps, "$GNTHS,341.3344,A*1F");
|
||||||
|
|
||||||
|
ASSERT_NEAR(gps.heading, 341.3344, 0.001, "heading");
|
||||||
|
ASSERT_EQ_INT(gps.heading_mode, 'A', "mode");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_parse_ths_not_valid(void)
|
||||||
|
{
|
||||||
|
TEST("THS: not valid mode");
|
||||||
|
reset_gps();
|
||||||
|
|
||||||
|
um982_parse_sentence(&gps, "$GNTHS,,V*10");
|
||||||
|
|
||||||
|
ASSERT_NAN(gps.heading, "heading should be NAN when empty");
|
||||||
|
ASSERT_EQ_INT(gps.heading_mode, 'V', "mode V");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_parse_ths_zero(void)
|
||||||
|
{
|
||||||
|
TEST("THS: heading exactly 0.0000");
|
||||||
|
reset_gps();
|
||||||
|
|
||||||
|
um982_parse_sentence(&gps, "$GNTHS,0.0000,A*19");
|
||||||
|
|
||||||
|
ASSERT_NEAR(gps.heading, 0.0, 0.001, "heading zero");
|
||||||
|
ASSERT_EQ_INT(gps.heading_mode, 'A', "mode A");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_parse_ths_360_boundary(void)
|
||||||
|
{
|
||||||
|
TEST("THS: heading near 360");
|
||||||
|
reset_gps();
|
||||||
|
|
||||||
|
um982_parse_sentence(&gps, "$GNTHS,359.9999,D*13");
|
||||||
|
|
||||||
|
ASSERT_NEAR(gps.heading, 359.9999, 0.001, "heading near 360");
|
||||||
|
ASSERT_EQ_INT(gps.heading_mode, 'D', "mode D");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================= VTG parsing tests ========================= */
|
||||||
|
|
||||||
|
static void test_parse_vtg(void)
|
||||||
|
{
|
||||||
|
TEST("VTG: course and speed");
|
||||||
|
reset_gps();
|
||||||
|
|
||||||
|
um982_parse_sentence(&gps,
|
||||||
|
"$GPVTG,220.86,T,,M,2.550,N,4.724,K,A*34");
|
||||||
|
|
||||||
|
ASSERT_NEAR(gps.course_true, 220.86, 0.01, "course");
|
||||||
|
ASSERT_NEAR(gps.speed_knots, 2.550, 0.001, "speed knots");
|
||||||
|
ASSERT_NEAR(gps.speed_kmh, 4.724, 0.001, "speed kmh");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================= Talker ID tests =========================== */
|
||||||
|
|
||||||
|
static void test_talker_gp(void)
|
||||||
|
{
|
||||||
|
TEST("talker: GP prefix parses correctly");
|
||||||
|
reset_gps();
|
||||||
|
|
||||||
|
um982_parse_sentence(&gps, "$GPTHS,123.4567,A*07");
|
||||||
|
|
||||||
|
ASSERT_NEAR(gps.heading, 123.4567, 0.001, "heading with GP");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_talker_gl(void)
|
||||||
|
{
|
||||||
|
TEST("talker: GL prefix parses correctly");
|
||||||
|
reset_gps();
|
||||||
|
|
||||||
|
um982_parse_sentence(&gps, "$GLTHS,123.4567,A*1B");
|
||||||
|
|
||||||
|
ASSERT_NEAR(gps.heading, 123.4567, 0.001, "heading with GL");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================= Feed / line assembly tests ================ */
|
||||||
|
|
||||||
|
static void test_feed_single_sentence(void)
|
||||||
|
{
|
||||||
|
TEST("feed: single complete sentence with CRLF");
|
||||||
|
reset_gps();
|
||||||
|
mock_set_tick(5000);
|
||||||
|
|
||||||
|
const char *data = "$GNTHS,341.3344,A*1F\r\n";
|
||||||
|
um982_feed(&gps, (const uint8_t *)data, (uint16_t)strlen(data));
|
||||||
|
|
||||||
|
ASSERT_NEAR(gps.heading, 341.3344, 0.001, "heading");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_feed_multiple_sentences(void)
|
||||||
|
{
|
||||||
|
TEST("feed: multiple sentences in one chunk");
|
||||||
|
reset_gps();
|
||||||
|
mock_set_tick(5000);
|
||||||
|
|
||||||
|
const char *data =
|
||||||
|
"$GNTHS,100.0000,A*18\r\n"
|
||||||
|
"$GNGGA,001043.00,4404.14036,N,12118.85961,W,1,12,0.98,1113.0,M,-21.3,M*47\r\n";
|
||||||
|
um982_feed(&gps, (const uint8_t *)data, (uint16_t)strlen(data));
|
||||||
|
|
||||||
|
ASSERT_NEAR(gps.heading, 100.0, 0.01, "heading from THS");
|
||||||
|
ASSERT_EQ_INT(gps.fix_quality, 1, "fix from GGA");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_feed_partial_then_complete(void)
|
||||||
|
{
|
||||||
|
TEST("feed: partial bytes then complete");
|
||||||
|
reset_gps();
|
||||||
|
mock_set_tick(5000);
|
||||||
|
|
||||||
|
const char *part1 = "$GNTHS,200.";
|
||||||
|
const char *part2 = "5000,A*1E\r\n";
|
||||||
|
um982_feed(&gps, (const uint8_t *)part1, (uint16_t)strlen(part1));
|
||||||
|
/* Heading should not be set yet */
|
||||||
|
ASSERT_NAN(gps.heading, "should be NAN before complete");
|
||||||
|
|
||||||
|
um982_feed(&gps, (const uint8_t *)part2, (uint16_t)strlen(part2));
|
||||||
|
ASSERT_NEAR(gps.heading, 200.5, 0.01, "heading after complete");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_feed_bad_checksum_rejected(void)
|
||||||
|
{
|
||||||
|
TEST("feed: bad checksum sentence is rejected");
|
||||||
|
reset_gps();
|
||||||
|
mock_set_tick(5000);
|
||||||
|
|
||||||
|
const char *data = "$GNTHS,999.0000,A*FF\r\n";
|
||||||
|
um982_feed(&gps, (const uint8_t *)data, (uint16_t)strlen(data));
|
||||||
|
|
||||||
|
ASSERT_NAN(gps.heading, "heading should remain NAN");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_feed_versiona_response(void)
|
||||||
|
{
|
||||||
|
TEST("feed: VERSIONA response sets flag");
|
||||||
|
reset_gps();
|
||||||
|
|
||||||
|
const char *data = "#VERSIONA,79,GPS,FINE,2326,378237000,15434,0,18,889;\"UM982\"\r\n";
|
||||||
|
um982_feed(&gps, (const uint8_t *)data, (uint16_t)strlen(data));
|
||||||
|
|
||||||
|
ASSERT_TRUE(gps.version_received, "version_received should be true");
|
||||||
|
ASSERT_TRUE(gps.initialized, "VERSIONA should mark communication alive");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================= Validity / age tests ====================== */
|
||||||
|
|
||||||
|
static void test_heading_valid_within_timeout(void)
|
||||||
|
{
|
||||||
|
TEST("validity: heading valid within timeout");
|
||||||
|
reset_gps();
|
||||||
|
mock_set_tick(10000);
|
||||||
|
|
||||||
|
um982_parse_sentence(&gps, "$GNTHS,341.3344,A*1F");
|
||||||
|
|
||||||
|
/* Still at tick 10000 */
|
||||||
|
ASSERT_TRUE(um982_is_heading_valid(&gps), "should be valid");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_heading_invalid_after_timeout(void)
|
||||||
|
{
|
||||||
|
TEST("validity: heading invalid after 2s timeout");
|
||||||
|
reset_gps();
|
||||||
|
mock_set_tick(10000);
|
||||||
|
|
||||||
|
um982_parse_sentence(&gps, "$GNTHS,341.3344,A*1F");
|
||||||
|
|
||||||
|
/* Advance past timeout */
|
||||||
|
mock_set_tick(12500);
|
||||||
|
ASSERT_FALSE(um982_is_heading_valid(&gps), "should be invalid after 2.5s");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_heading_invalid_mode_v(void)
|
||||||
|
{
|
||||||
|
TEST("validity: heading invalid with mode V");
|
||||||
|
reset_gps();
|
||||||
|
mock_set_tick(10000);
|
||||||
|
|
||||||
|
um982_parse_sentence(&gps, "$GNTHS,,V*10");
|
||||||
|
|
||||||
|
ASSERT_FALSE(um982_is_heading_valid(&gps), "mode V is invalid");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_position_valid(void)
|
||||||
|
{
|
||||||
|
TEST("validity: position valid with fix quality 1");
|
||||||
|
reset_gps();
|
||||||
|
mock_set_tick(10000);
|
||||||
|
|
||||||
|
um982_parse_sentence(&gps,
|
||||||
|
"$GNGGA,001043.00,4404.14036,N,12118.85961,W,1,12,0.98,1113.0,M,-21.3,M*47");
|
||||||
|
|
||||||
|
ASSERT_TRUE(um982_is_position_valid(&gps), "should be valid");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_position_invalid_no_fix(void)
|
||||||
|
{
|
||||||
|
TEST("validity: position invalid with no fix");
|
||||||
|
reset_gps();
|
||||||
|
mock_set_tick(10000);
|
||||||
|
|
||||||
|
um982_parse_sentence(&gps,
|
||||||
|
"$GNGGA,235959.00,,,,,0,00,99.99,,,,,,*79");
|
||||||
|
|
||||||
|
ASSERT_FALSE(um982_is_position_valid(&gps), "no fix = invalid");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_position_age_uses_last_valid_fix(void)
|
||||||
|
{
|
||||||
|
TEST("age: position age uses last valid fix, not no-fix GGA");
|
||||||
|
reset_gps();
|
||||||
|
|
||||||
|
mock_set_tick(10000);
|
||||||
|
um982_parse_sentence(&gps,
|
||||||
|
"$GNGGA,001043.00,4404.14036,N,12118.85961,W,1,12,0.98,1113.0,M,-21.3,M*47");
|
||||||
|
|
||||||
|
mock_set_tick(12000);
|
||||||
|
um982_parse_sentence(&gps,
|
||||||
|
"$GNGGA,235959.00,,,,,0,00,99.99,,,,,,*79");
|
||||||
|
|
||||||
|
mock_set_tick(12500);
|
||||||
|
ASSERT_EQ_INT(um982_position_age(&gps), 2500, "age should still be from last valid fix");
|
||||||
|
ASSERT_FALSE(um982_is_position_valid(&gps), "latest no-fix GGA should invalidate position");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_heading_age(void)
|
||||||
|
{
|
||||||
|
TEST("age: heading age computed correctly");
|
||||||
|
reset_gps();
|
||||||
|
mock_set_tick(10000);
|
||||||
|
|
||||||
|
um982_parse_sentence(&gps, "$GNTHS,341.3344,A*1F");
|
||||||
|
|
||||||
|
mock_set_tick(10500);
|
||||||
|
uint32_t age = um982_heading_age(&gps);
|
||||||
|
ASSERT_EQ_INT(age, 500, "age should be 500ms");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================= Send command tests ======================== */
|
||||||
|
|
||||||
|
static void test_send_command_appends_crlf(void)
|
||||||
|
{
|
||||||
|
TEST("send_command: appends \\r\\n");
|
||||||
|
reset_gps();
|
||||||
|
|
||||||
|
um982_send_command(&gps, "GPGGA COM2 1");
|
||||||
|
|
||||||
|
/* Check that TX buffer contains "GPGGA COM2 1\r\n" */
|
||||||
|
const char *expected = "GPGGA COM2 1\r\n";
|
||||||
|
ASSERT_TRUE(mock_uart_tx_len == strlen(expected), "TX length");
|
||||||
|
ASSERT_TRUE(memcmp(mock_uart_tx_buf, expected, strlen(expected)) == 0,
|
||||||
|
"TX content should be 'GPGGA COM2 1\\r\\n'");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_send_command_null_safety(void)
|
||||||
|
{
|
||||||
|
TEST("send_command: NULL gps returns false");
|
||||||
|
ASSERT_FALSE(um982_send_command(NULL, "RESET"), "should return false");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================= Init sequence tests ======================= */
|
||||||
|
|
||||||
|
static void test_init_sends_correct_commands(void)
|
||||||
|
{
|
||||||
|
TEST("init: sends correct command sequence");
|
||||||
|
spy_reset();
|
||||||
|
mock_uart_tx_clear();
|
||||||
|
|
||||||
|
/* Pre-load VERSIONA response so init succeeds */
|
||||||
|
const char *ver_resp = "#VERSIONA,79,GPS,FINE,2326,378237000,15434,0,18,889;\"UM982\"\r\n";
|
||||||
|
mock_uart_rx_load(&huart5, (const uint8_t *)ver_resp, (uint16_t)strlen(ver_resp));
|
||||||
|
|
||||||
|
UM982_GPS_t init_gps;
|
||||||
|
bool ok = um982_init(&init_gps, &huart5, 50.0f, 3.0f);
|
||||||
|
|
||||||
|
ASSERT_TRUE(ok, "init should succeed");
|
||||||
|
ASSERT_TRUE(init_gps.initialized, "should be initialized");
|
||||||
|
|
||||||
|
/* Verify TX buffer contains expected commands */
|
||||||
|
const char *tx = (const char *)mock_uart_tx_buf;
|
||||||
|
ASSERT_TRUE(strstr(tx, "UNLOG\r\n") != NULL, "should send UNLOG");
|
||||||
|
ASSERT_TRUE(strstr(tx, "CONFIG HEADING FIXLENGTH\r\n") != NULL, "should send CONFIG HEADING");
|
||||||
|
ASSERT_TRUE(strstr(tx, "CONFIG HEADING LENGTH 50 3\r\n") != NULL, "should send LENGTH");
|
||||||
|
ASSERT_TRUE(strstr(tx, "GPGGA COM2 1\r\n") != NULL, "should enable GGA");
|
||||||
|
ASSERT_TRUE(strstr(tx, "GPRMC COM2 1\r\n") != NULL, "should enable RMC");
|
||||||
|
ASSERT_TRUE(strstr(tx, "GPTHS COM2 0.2\r\n") != NULL, "should enable THS at 5Hz");
|
||||||
|
ASSERT_TRUE(strstr(tx, "SAVECONFIG\r\n") == NULL, "should NOT save config (NVM wear)");
|
||||||
|
ASSERT_TRUE(strstr(tx, "VERSIONA\r\n") != NULL, "should query version");
|
||||||
|
|
||||||
|
/* Verify command order: UNLOG should come before GPGGA */
|
||||||
|
const char *unlog_pos = strstr(tx, "UNLOG\r\n");
|
||||||
|
const char *gpgga_pos = strstr(tx, "GPGGA COM2 1\r\n");
|
||||||
|
ASSERT_TRUE(unlog_pos < gpgga_pos, "UNLOG should precede GPGGA");
|
||||||
|
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_init_no_baseline(void)
|
||||||
|
{
|
||||||
|
TEST("init: baseline=0 skips LENGTH command");
|
||||||
|
spy_reset();
|
||||||
|
mock_uart_tx_clear();
|
||||||
|
|
||||||
|
const char *ver_resp = "#VERSIONA,79,GPS,FINE,2326,378237000,15434,0,18,889;\"UM982\"\r\n";
|
||||||
|
mock_uart_rx_load(&huart5, (const uint8_t *)ver_resp, (uint16_t)strlen(ver_resp));
|
||||||
|
|
||||||
|
UM982_GPS_t init_gps;
|
||||||
|
um982_init(&init_gps, &huart5, 0.0f, 0.0f);
|
||||||
|
|
||||||
|
const char *tx = (const char *)mock_uart_tx_buf;
|
||||||
|
ASSERT_TRUE(strstr(tx, "CONFIG HEADING LENGTH") == NULL, "should NOT send LENGTH");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_init_fails_no_version(void)
|
||||||
|
{
|
||||||
|
TEST("init: fails if no VERSIONA response");
|
||||||
|
spy_reset();
|
||||||
|
mock_uart_tx_clear();
|
||||||
|
|
||||||
|
/* Don't load any RX data — init should timeout */
|
||||||
|
UM982_GPS_t init_gps;
|
||||||
|
bool ok = um982_init(&init_gps, &huart5, 50.0f, 3.0f);
|
||||||
|
|
||||||
|
ASSERT_FALSE(ok, "init should fail without version response");
|
||||||
|
ASSERT_FALSE(init_gps.initialized, "should not be initialized");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_nmea_traffic_sets_initialized_without_versiona(void)
|
||||||
|
{
|
||||||
|
TEST("init state: supported NMEA traffic sets initialized");
|
||||||
|
reset_gps();
|
||||||
|
|
||||||
|
ASSERT_FALSE(gps.initialized, "should start uninitialized");
|
||||||
|
um982_parse_sentence(&gps, "$GNTHS,341.3344,A*1F");
|
||||||
|
ASSERT_TRUE(gps.initialized, "supported NMEA should mark communication alive");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================= Edge case tests =========================== */
|
||||||
|
|
||||||
|
static void test_empty_fields_handled(void)
|
||||||
|
{
|
||||||
|
TEST("edge: GGA with empty lat/lon fields");
|
||||||
|
reset_gps();
|
||||||
|
gps.latitude = 99.99;
|
||||||
|
gps.longitude = 99.99;
|
||||||
|
|
||||||
|
/* GGA with empty position fields (no fix) */
|
||||||
|
um982_parse_sentence(&gps,
|
||||||
|
"$GNGGA,235959.00,,,,,0,00,99.99,,,,,,*79");
|
||||||
|
|
||||||
|
ASSERT_EQ_INT(gps.fix_quality, 0, "no fix");
|
||||||
|
/* Latitude/longitude should not be updated (fields are empty) */
|
||||||
|
ASSERT_NEAR(gps.latitude, 99.99, 0.01, "lat unchanged");
|
||||||
|
ASSERT_NEAR(gps.longitude, 99.99, 0.01, "lon unchanged");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_sentence_too_short(void)
|
||||||
|
{
|
||||||
|
TEST("edge: sentence too short to have formatter");
|
||||||
|
reset_gps();
|
||||||
|
/* Should not crash */
|
||||||
|
um982_parse_sentence(&gps, "$GN");
|
||||||
|
um982_parse_sentence(&gps, "$");
|
||||||
|
um982_parse_sentence(&gps, "");
|
||||||
|
um982_parse_sentence(&gps, NULL);
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_line_overflow(void)
|
||||||
|
{
|
||||||
|
TEST("edge: oversized line is dropped");
|
||||||
|
reset_gps();
|
||||||
|
|
||||||
|
/* Create a line longer than UM982_LINE_BUF_SIZE */
|
||||||
|
char big[200];
|
||||||
|
memset(big, 'X', sizeof(big));
|
||||||
|
big[0] = '$';
|
||||||
|
big[198] = '\n';
|
||||||
|
big[199] = '\0';
|
||||||
|
|
||||||
|
um982_feed(&gps, (const uint8_t *)big, 199);
|
||||||
|
/* Should not crash, heading should still be NAN */
|
||||||
|
ASSERT_NAN(gps.heading, "no valid data from overflow");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_process_via_mock_uart(void)
|
||||||
|
{
|
||||||
|
TEST("process: reads from mock UART RX buffer");
|
||||||
|
reset_gps();
|
||||||
|
mock_set_tick(5000);
|
||||||
|
|
||||||
|
/* Load data into mock UART RX */
|
||||||
|
const char *data = "$GNTHS,275.1234,D*18\r\n";
|
||||||
|
mock_uart_rx_load(&huart5, (const uint8_t *)data, (uint16_t)strlen(data));
|
||||||
|
|
||||||
|
/* Call process() which reads from UART */
|
||||||
|
um982_process(&gps);
|
||||||
|
|
||||||
|
ASSERT_NEAR(gps.heading, 275.1234, 0.001, "heading via process()");
|
||||||
|
ASSERT_EQ_INT(gps.heading_mode, 'D', "mode D");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================= PR #68 bug regression tests =============== */
|
||||||
|
|
||||||
|
/* These tests specifically verify the bugs found in the reverted PR #68 */
|
||||||
|
|
||||||
|
static void test_regression_sentence_id_with_gn_prefix(void)
|
||||||
|
{
|
||||||
|
TEST("regression: GN-prefixed GGA is correctly identified");
|
||||||
|
reset_gps();
|
||||||
|
|
||||||
|
/* PR #68 bug: strncmp(sentence, "GGA", 3) compared "GNG" vs "GGA" — never matched.
|
||||||
|
* Our fix: skip 2-char talker ID, compare at sentence+3. */
|
||||||
|
um982_parse_sentence(&gps,
|
||||||
|
"$GNGGA,001043.00,4404.14036,N,12118.85961,W,1,12,0.98,1113.0,M,-21.3,M*47");
|
||||||
|
|
||||||
|
ASSERT_EQ_INT(gps.fix_quality, 1, "GGA should parse with GN prefix");
|
||||||
|
ASSERT_NEAR(gps.latitude, 44.069006, 0.001, "latitude should be parsed");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_regression_longitude_3digit_degrees(void)
|
||||||
|
{
|
||||||
|
TEST("regression: 3-digit longitude degrees parsed correctly");
|
||||||
|
reset_gps();
|
||||||
|
|
||||||
|
/* PR #68 bug: hardcoded 2-digit degrees for longitude.
|
||||||
|
* 12118.85961 should be 121° 18.85961' = 121.314327° */
|
||||||
|
um982_parse_sentence(&gps,
|
||||||
|
"$GNGGA,001043.00,4404.14036,N,12118.85961,W,1,12,0.98,1113.0,M,-21.3,M*47");
|
||||||
|
|
||||||
|
ASSERT_NEAR(gps.longitude, -(121.0 + 18.85961/60.0), 0.0001,
|
||||||
|
"longitude 121° should not be parsed as 12°");
|
||||||
|
ASSERT_TRUE(gps.longitude < -100.0, "longitude should be > 100 degrees");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_regression_hemisphere_no_ptr_corrupt(void)
|
||||||
|
{
|
||||||
|
TEST("regression: hemisphere parsing doesn't corrupt field pointer");
|
||||||
|
reset_gps();
|
||||||
|
|
||||||
|
/* PR #68 bug: GGA/RMC hemisphere cases manually advanced ptr,
|
||||||
|
* desynchronizing from field counter. Our parser uses proper tokenizer. */
|
||||||
|
um982_parse_sentence(&gps,
|
||||||
|
"$GNGGA,001043.00,4404.14036,N,12118.85961,W,1,12,0.98,1113.0,M,-21.3,M*47");
|
||||||
|
|
||||||
|
/* After lat/lon, remaining fields should be correct */
|
||||||
|
ASSERT_EQ_INT(gps.num_satellites, 12, "sats after hemisphere");
|
||||||
|
ASSERT_NEAR(gps.hdop, 0.98, 0.01, "hdop after hemisphere");
|
||||||
|
ASSERT_NEAR(gps.altitude, 1113.0, 0.1, "altitude after hemisphere");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_regression_rmc_also_parsed(void)
|
||||||
|
{
|
||||||
|
TEST("regression: RMC sentence is actually parsed (not dead code)");
|
||||||
|
reset_gps();
|
||||||
|
|
||||||
|
/* PR #68 bug: identifySentence never matched GGA/RMC, so position
|
||||||
|
* parsing was dead code. */
|
||||||
|
um982_parse_sentence(&gps,
|
||||||
|
"$GNRMC,001031.00,A,4404.13993,N,12118.86023,W,0.146,,100117,,,A*7B");
|
||||||
|
|
||||||
|
ASSERT_TRUE(gps.latitude > 44.0, "RMC lat should be parsed");
|
||||||
|
ASSERT_TRUE(gps.longitude < -121.0, "RMC lon should be parsed");
|
||||||
|
ASSERT_NEAR(gps.speed_knots, 0.146, 0.001, "RMC speed");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================= Main ====================================== */
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
printf("=== UM982 GPS Driver Tests ===\n\n");
|
||||||
|
|
||||||
|
printf("--- Checksum ---\n");
|
||||||
|
test_checksum_valid();
|
||||||
|
test_checksum_valid_ths();
|
||||||
|
test_checksum_invalid();
|
||||||
|
test_checksum_missing_star();
|
||||||
|
test_checksum_null();
|
||||||
|
test_checksum_no_dollar();
|
||||||
|
|
||||||
|
printf("\n--- Coordinate Parsing ---\n");
|
||||||
|
test_coord_latitude_north();
|
||||||
|
test_coord_latitude_south();
|
||||||
|
test_coord_longitude_3digit();
|
||||||
|
test_coord_longitude_east();
|
||||||
|
test_coord_empty();
|
||||||
|
test_coord_null();
|
||||||
|
test_coord_no_dot();
|
||||||
|
|
||||||
|
printf("\n--- GGA Parsing ---\n");
|
||||||
|
test_parse_gga_full();
|
||||||
|
test_parse_gga_rtk_fixed();
|
||||||
|
test_parse_gga_no_fix();
|
||||||
|
|
||||||
|
printf("\n--- RMC Parsing ---\n");
|
||||||
|
test_parse_rmc_valid();
|
||||||
|
test_parse_rmc_void();
|
||||||
|
|
||||||
|
printf("\n--- THS Parsing ---\n");
|
||||||
|
test_parse_ths_autonomous();
|
||||||
|
test_parse_ths_not_valid();
|
||||||
|
test_parse_ths_zero();
|
||||||
|
test_parse_ths_360_boundary();
|
||||||
|
|
||||||
|
printf("\n--- VTG Parsing ---\n");
|
||||||
|
test_parse_vtg();
|
||||||
|
|
||||||
|
printf("\n--- Talker IDs ---\n");
|
||||||
|
test_talker_gp();
|
||||||
|
test_talker_gl();
|
||||||
|
|
||||||
|
printf("\n--- Feed / Line Assembly ---\n");
|
||||||
|
test_feed_single_sentence();
|
||||||
|
test_feed_multiple_sentences();
|
||||||
|
test_feed_partial_then_complete();
|
||||||
|
test_feed_bad_checksum_rejected();
|
||||||
|
test_feed_versiona_response();
|
||||||
|
|
||||||
|
printf("\n--- Validity / Age ---\n");
|
||||||
|
test_heading_valid_within_timeout();
|
||||||
|
test_heading_invalid_after_timeout();
|
||||||
|
test_heading_invalid_mode_v();
|
||||||
|
test_position_valid();
|
||||||
|
test_position_invalid_no_fix();
|
||||||
|
test_position_age_uses_last_valid_fix();
|
||||||
|
test_heading_age();
|
||||||
|
|
||||||
|
printf("\n--- Send Command ---\n");
|
||||||
|
test_send_command_appends_crlf();
|
||||||
|
test_send_command_null_safety();
|
||||||
|
|
||||||
|
printf("\n--- Init Sequence ---\n");
|
||||||
|
test_init_sends_correct_commands();
|
||||||
|
test_init_no_baseline();
|
||||||
|
test_init_fails_no_version();
|
||||||
|
test_nmea_traffic_sets_initialized_without_versiona();
|
||||||
|
|
||||||
|
printf("\n--- Edge Cases ---\n");
|
||||||
|
test_empty_fields_handled();
|
||||||
|
test_sentence_too_short();
|
||||||
|
test_line_overflow();
|
||||||
|
test_process_via_mock_uart();
|
||||||
|
|
||||||
|
printf("\n--- PR #68 Regression ---\n");
|
||||||
|
test_regression_sentence_id_with_gn_prefix();
|
||||||
|
test_regression_longitude_3digit_degrees();
|
||||||
|
test_regression_hemisphere_no_ptr_corrupt();
|
||||||
|
test_regression_rmc_also_parsed();
|
||||||
|
|
||||||
|
printf("\n===============================================\n");
|
||||||
|
printf(" Results: %d passed, %d failed (of %d total)\n",
|
||||||
|
tests_passed, tests_failed, tests_passed + tests_failed);
|
||||||
|
printf("===============================================\n");
|
||||||
|
|
||||||
|
return tests_failed > 0 ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -224,7 +224,7 @@ set_property IOSTANDARD LVCMOS33 [get_ports {stm32_mixers_enable}]
|
|||||||
|
|
||||||
# DIG_5 = H11, DIG_6 = G12, DIG_7 = H12 — FPGA→STM32 status outputs
|
# DIG_5 = H11, DIG_6 = G12, DIG_7 = H12 — FPGA→STM32 status outputs
|
||||||
# DIG_5: AGC saturation flag (PD13 on STM32)
|
# DIG_5: AGC saturation flag (PD13 on STM32)
|
||||||
# DIG_6: reserved (PD14)
|
# DIG_6: AGC enable flag (PD14) — mirrors FPGA host_agc_enable to STM32
|
||||||
# DIG_7: reserved (PD15)
|
# DIG_7: reserved (PD15)
|
||||||
set_property PACKAGE_PIN H11 [get_ports {gpio_dig5}]
|
set_property PACKAGE_PIN H11 [get_ports {gpio_dig5}]
|
||||||
set_property PACKAGE_PIN G12 [get_ports {gpio_dig6}]
|
set_property PACKAGE_PIN G12 [get_ports {gpio_dig6}]
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ module radar_receiver_final (
|
|||||||
input wire adc_dco_n, // Data Clock Output N (400MHz LVDS)
|
input wire adc_dco_n, // Data Clock Output N (400MHz LVDS)
|
||||||
output wire adc_pwdn,
|
output wire adc_pwdn,
|
||||||
|
|
||||||
// Chirp counter from transmitter (for frame sync and matched filter)
|
// Chirp counter from transmitter (for matched filter indexing)
|
||||||
input wire [5:0] chirp_counter,
|
input wire [5:0] chirp_counter,
|
||||||
|
// Frame-start pulse from transmitter (CDC-synchronized, 1 clk_100m cycle)
|
||||||
|
input wire tx_frame_start,
|
||||||
|
|
||||||
output wire [31:0] doppler_output,
|
output wire [31:0] doppler_output,
|
||||||
output wire doppler_valid,
|
output wire doppler_valid,
|
||||||
@@ -392,32 +394,31 @@ mti_canceller #(
|
|||||||
.mti_first_chirp(mti_first_chirp)
|
.mti_first_chirp(mti_first_chirp)
|
||||||
);
|
);
|
||||||
|
|
||||||
// ========== FRAME SYNC USING chirp_counter ==========
|
// ========== FRAME SYNC FROM TRANSMITTER ==========
|
||||||
reg [5:0] chirp_counter_prev;
|
// [FPGA-001 FIXED] Use the authoritative new_chirp_frame signal from the
|
||||||
|
// transmitter (via plfm_chirp_controller_enhanced), CDC-synchronized to
|
||||||
|
// clk_100m in radar_system_top. Previous code tried to derive frame
|
||||||
|
// boundaries from chirp_counter == 0, but that counter comes from the
|
||||||
|
// transmitter path (plfm_chirp_controller_enhanced) which does NOT wrap
|
||||||
|
// at chirps_per_elev — it overflows to N and only wraps at 6-bit rollover
|
||||||
|
// (64). This caused frame pulses at half the expected rate for N=32.
|
||||||
|
reg tx_frame_start_prev;
|
||||||
reg new_frame_pulse;
|
reg new_frame_pulse;
|
||||||
|
|
||||||
always @(posedge clk or negedge reset_n) begin
|
always @(posedge clk or negedge reset_n) begin
|
||||||
if (!reset_n) begin
|
if (!reset_n) begin
|
||||||
chirp_counter_prev <= 6'd0;
|
tx_frame_start_prev <= 1'b0;
|
||||||
new_frame_pulse <= 1'b0;
|
new_frame_pulse <= 1'b0;
|
||||||
end else begin
|
end else begin
|
||||||
// Default: no pulse
|
|
||||||
new_frame_pulse <= 1'b0;
|
new_frame_pulse <= 1'b0;
|
||||||
|
|
||||||
// Dynamic frame detection using host_chirps_per_elev.
|
// Edge detect: tx_frame_start is a toggle-CDC derived pulse that
|
||||||
// Detect frame boundary when chirp_counter changes AND is a
|
// may be 1 clock wide. Capture rising edge for clean 1-cycle pulse.
|
||||||
// multiple of host_chirps_per_elev (0, N, 2N, 3N, ...).
|
if (tx_frame_start && !tx_frame_start_prev) begin
|
||||||
// Uses a modulo counter that resets at host_chirps_per_elev.
|
new_frame_pulse <= 1'b1;
|
||||||
if (chirp_counter != chirp_counter_prev) begin
|
|
||||||
if (chirp_counter == 6'd0 ||
|
|
||||||
chirp_counter == host_chirps_per_elev ||
|
|
||||||
chirp_counter == {host_chirps_per_elev, 1'b0}) begin
|
|
||||||
new_frame_pulse <= 1'b1;
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
// Store previous value
|
tx_frame_start_prev <= tx_frame_start;
|
||||||
chirp_counter_prev <= chirp_counter;
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -483,14 +484,6 @@ always @(posedge clk or negedge reset_n) begin
|
|||||||
`endif
|
`endif
|
||||||
chirps_in_current_frame <= 0;
|
chirps_in_current_frame <= 0;
|
||||||
end
|
end
|
||||||
|
|
||||||
// Monitor chirp counter pattern
|
|
||||||
if (chirp_counter != chirp_counter_prev) begin
|
|
||||||
`ifdef SIMULATION
|
|
||||||
$display("[TOP] chirp_counter: %0d ? %0d",
|
|
||||||
chirp_counter_prev, chirp_counter);
|
|
||||||
`endif
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ module radar_system_top (
|
|||||||
// FPGA→STM32 GPIO outputs (DIG_5..DIG_7 on 50T board)
|
// FPGA→STM32 GPIO outputs (DIG_5..DIG_7 on 50T board)
|
||||||
// Used by STM32 outer AGC loop to read saturation state without USB polling.
|
// Used by STM32 outer AGC loop to read saturation state without USB polling.
|
||||||
output wire gpio_dig5, // DIG_5 (H11→PD13): AGC saturation flag (1=clipping detected)
|
output wire gpio_dig5, // DIG_5 (H11→PD13): AGC saturation flag (1=clipping detected)
|
||||||
output wire gpio_dig6, // DIG_6 (G12→PD14): reserved (tied low)
|
output wire gpio_dig6, // DIG_6 (G12→PD14): AGC enable flag (mirrors host_agc_enable)
|
||||||
output wire gpio_dig7 // DIG_7 (H12→PD15): reserved (tied low)
|
output wire gpio_dig7 // DIG_7 (H12→PD15): reserved (tied low)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -505,6 +505,8 @@ radar_receiver_final rx_inst (
|
|||||||
|
|
||||||
// Chirp counter from transmitter (CDC-synchronized from 120 MHz domain)
|
// Chirp counter from transmitter (CDC-synchronized from 120 MHz domain)
|
||||||
.chirp_counter(tx_current_chirp_sync),
|
.chirp_counter(tx_current_chirp_sync),
|
||||||
|
// Frame-start pulse from transmitter (CDC-synchronized toggle→pulse)
|
||||||
|
.tx_frame_start(tx_new_chirp_frame_sync),
|
||||||
|
|
||||||
// ADC Physical Interface
|
// ADC Physical Interface
|
||||||
.adc_d_p(adc_d_p),
|
.adc_d_p(adc_d_p),
|
||||||
@@ -1035,9 +1037,11 @@ assign system_status = status_reg;
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
// DIG_5: AGC saturation flag — high when per-frame saturation_count > 0.
|
// DIG_5: AGC saturation flag — high when per-frame saturation_count > 0.
|
||||||
// STM32 reads PD13 to detect clipping and adjust ADAR1000 VGA gain.
|
// STM32 reads PD13 to detect clipping and adjust ADAR1000 VGA gain.
|
||||||
// DIG_6, DIG_7: Reserved (tied low for future use).
|
// DIG_6: AGC enable flag — mirrors host_agc_enable so STM32 outer-loop AGC
|
||||||
|
// tracks the FPGA register as single source of truth.
|
||||||
|
// DIG_7: Reserved (tied low for future use).
|
||||||
assign gpio_dig5 = (rx_agc_saturation_count != 8'd0);
|
assign gpio_dig5 = (rx_agc_saturation_count != 8'd0);
|
||||||
assign gpio_dig6 = 1'b0;
|
assign gpio_dig6 = host_agc_enable;
|
||||||
assign gpio_dig7 = 1'b0;
|
assign gpio_dig7 = 1'b0;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -291,9 +291,12 @@ class Mixer:
|
|||||||
Convert 8-bit unsigned ADC to 18-bit signed.
|
Convert 8-bit unsigned ADC to 18-bit signed.
|
||||||
RTL: adc_signed_w = {1'b0, adc_data, {9{1'b0}}} -
|
RTL: adc_signed_w = {1'b0, adc_data, {9{1'b0}}} -
|
||||||
{1'b0, {8{1'b1}}, {9{1'b0}}} / 2
|
{1'b0, {8{1'b1}}, {9{1'b0}}} / 2
|
||||||
= (adc_data << 9) - (0xFF << 9) / 2
|
|
||||||
= (adc_data << 9) - (0xFF << 8) [integer division]
|
Verilog '/' binds tighter than '-', so the division applies
|
||||||
= (adc_data << 9) - 0x7F80
|
only to the second concatenation:
|
||||||
|
{1'b0, 8'hFF, 9'b0} = 0x1FE00
|
||||||
|
0x1FE00 / 2 = 0xFF00 = 65280
|
||||||
|
Result: (adc_data << 9) - 0xFF00
|
||||||
"""
|
"""
|
||||||
adc_data_8bit = adc_data_8bit & 0xFF
|
adc_data_8bit = adc_data_8bit & 0xFF
|
||||||
# {1'b0, adc_data, 9'b0} = adc_data << 9, zero-padded to 18 bits
|
# {1'b0, adc_data, 9'b0} = adc_data << 9, zero-padded to 18 bits
|
||||||
|
|||||||
@@ -290,9 +290,9 @@ def run_ddc(adc_samples):
|
|||||||
for n in range(n_samples):
|
for n in range(n_samples):
|
||||||
# ADC sign conversion: RTL does offset binary → signed 18-bit
|
# ADC sign conversion: RTL does offset binary → signed 18-bit
|
||||||
# adc_signed_w = {1'b0, adc_data, 9'b0} - {1'b0, 8'hFF, 9'b0}/2
|
# adc_signed_w = {1'b0, adc_data, 9'b0} - {1'b0, 8'hFF, 9'b0}/2
|
||||||
# Simplified: center around zero, scale to 18-bit
|
# Exact: (adc_val << 9) - 0xFF00, where 0xFF00 = {1'b0,8'hFF,9'b0}/2
|
||||||
adc_val = int(adc_samples[n])
|
adc_val = int(adc_samples[n])
|
||||||
adc_signed = (adc_val - 128) << 9 # Approximate RTL sign conversion to 18-bit
|
adc_signed = (adc_val << 9) - 0xFF00 # Exact RTL: {1'b0,adc,9'b0} - {1'b0,8'hFF,9'b0}/2
|
||||||
adc_signed = saturate(adc_signed, 18)
|
adc_signed = saturate(adc_signed, 18)
|
||||||
|
|
||||||
# NCO lookup (ignoring dithering for golden reference)
|
# NCO lookup (ignoring dithering for golden reference)
|
||||||
|
|||||||
@@ -96,15 +96,31 @@ end
|
|||||||
reg [5:0] chirp_counter;
|
reg [5:0] chirp_counter;
|
||||||
reg mc_new_chirp_prev;
|
reg mc_new_chirp_prev;
|
||||||
|
|
||||||
|
// Frame-start pulse: mirrors the real transmitter's new_chirp_frame signal.
|
||||||
|
// In the real system this fires on IDLE→LONG_CHIRP transitions in the chirp
|
||||||
|
// controller. Here we derive it from the mode controller's chirp_count
|
||||||
|
// wrapping back to 0 (which wraps correctly at cfg_chirps_per_elev).
|
||||||
|
reg tx_frame_start;
|
||||||
|
reg [5:0] rmc_chirp_prev;
|
||||||
|
|
||||||
always @(posedge clk_100m or negedge reset_n) begin
|
always @(posedge clk_100m or negedge reset_n) begin
|
||||||
if (!reset_n) begin
|
if (!reset_n) begin
|
||||||
chirp_counter <= 6'd0;
|
chirp_counter <= 6'd0;
|
||||||
mc_new_chirp_prev <= 1'b0;
|
mc_new_chirp_prev <= 1'b0;
|
||||||
|
tx_frame_start <= 1'b0;
|
||||||
|
rmc_chirp_prev <= 6'd0;
|
||||||
end else begin
|
end else begin
|
||||||
mc_new_chirp_prev <= dut.mc_new_chirp;
|
mc_new_chirp_prev <= dut.mc_new_chirp;
|
||||||
if (dut.mc_new_chirp != mc_new_chirp_prev) begin
|
if (dut.mc_new_chirp != mc_new_chirp_prev) begin
|
||||||
chirp_counter <= chirp_counter + 1;
|
chirp_counter <= chirp_counter + 1;
|
||||||
end
|
end
|
||||||
|
|
||||||
|
// Detect when the internal mode controller's chirp_count wraps to 0
|
||||||
|
tx_frame_start <= 1'b0;
|
||||||
|
if (dut.rmc_chirp_count == 6'd0 && rmc_chirp_prev != 6'd0) begin
|
||||||
|
tx_frame_start <= 1'b1;
|
||||||
|
end
|
||||||
|
rmc_chirp_prev <= dut.rmc_chirp_count;
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -128,6 +144,7 @@ radar_receiver_final dut (
|
|||||||
.adc_pwdn(),
|
.adc_pwdn(),
|
||||||
|
|
||||||
.chirp_counter(chirp_counter),
|
.chirp_counter(chirp_counter),
|
||||||
|
.tx_frame_start(tx_frame_start),
|
||||||
|
|
||||||
.doppler_output(doppler_output),
|
.doppler_output(doppler_output),
|
||||||
.doppler_valid(doppler_valid),
|
.doppler_valid(doppler_valid),
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -497,6 +497,7 @@ def count_concat_bits(concat_expr: str, port_widths: dict[str, int]) -> ConcatWi
|
|||||||
# Unknown width — flag it
|
# Unknown width — flag it
|
||||||
fragments.append((part, -1))
|
fragments.append((part, -1))
|
||||||
total = -1 # Can't compute
|
total = -1 # Can't compute
|
||||||
|
break
|
||||||
|
|
||||||
return ConcatWidth(
|
return ConcatWidth(
|
||||||
total_bits=total,
|
total_bits=total,
|
||||||
|
|||||||
@@ -26,11 +26,14 @@ layers agree (because both could be wrong).
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import struct
|
import struct
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import ClassVar
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -40,6 +43,7 @@ import sys
|
|||||||
THIS_DIR = Path(__file__).resolve().parent
|
THIS_DIR = Path(__file__).resolve().parent
|
||||||
sys.path.insert(0, str(THIS_DIR))
|
sys.path.insert(0, str(THIS_DIR))
|
||||||
import contract_parser as cp # noqa: E402
|
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
|
# Also add the GUI dir to import radar_protocol
|
||||||
sys.path.insert(0, str(cp.GUI_DIR))
|
sys.path.insert(0, str(cp.GUI_DIR))
|
||||||
@@ -49,8 +53,8 @@ sys.path.insert(0, str(cp.GUI_DIR))
|
|||||||
# Helpers
|
# Helpers
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
|
||||||
IVERILOG = os.environ.get("IVERILOG", "/opt/homebrew/bin/iverilog")
|
IVERILOG = os.environ.get("IVERILOG", "iverilog")
|
||||||
VVP = os.environ.get("VVP", "/opt/homebrew/bin/vvp")
|
VVP = os.environ.get("VVP", "vvp")
|
||||||
CXX = os.environ.get("CXX", "c++")
|
CXX = os.environ.get("CXX", "c++")
|
||||||
|
|
||||||
# Check tool availability for conditional skipping
|
# Check tool availability for conditional skipping
|
||||||
@@ -61,6 +65,92 @@ _has_cxx = subprocess.run(
|
|||||||
[CXX, "--version"], capture_output=True
|
[CXX, "--version"], capture_output=True
|
||||||
).returncode == 0
|
).returncode == 0
|
||||||
|
|
||||||
|
# In CI, missing tools must be a hard failure — never silently skip.
|
||||||
|
_in_ci = os.environ.get("GITHUB_ACTIONS") == "true"
|
||||||
|
if _in_ci:
|
||||||
|
if not _has_iverilog:
|
||||||
|
raise RuntimeError(
|
||||||
|
"iverilog is required in CI but was not found. "
|
||||||
|
"Ensure 'apt-get install iverilog' ran and IVERILOG/VVP are on PATH."
|
||||||
|
)
|
||||||
|
if not _has_cxx:
|
||||||
|
raise RuntimeError(
|
||||||
|
"C++ compiler is required in CI but was not found. "
|
||||||
|
"Ensure build-essential is installed."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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]]:
|
def _parse_hex_results(text: str) -> list[dict[str, str]]:
|
||||||
"""Parse space-separated hex lines from TB output files."""
|
"""Parse space-separated hex lines from TB output files."""
|
||||||
@@ -355,6 +445,602 @@ class TestTier1ResetDefaults:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTier1AgcCrossLayerInvariant:
|
||||||
|
"""
|
||||||
|
Verify AGC enable/disable is consistent across FPGA, MCU, and GUI layers.
|
||||||
|
|
||||||
|
System-level invariant: the FPGA register host_agc_enable is the single
|
||||||
|
source of truth for AGC state. It propagates to MCU via DIG_6 GPIO and
|
||||||
|
to GUI via status word 4 bit[11]. At boot, all layers must agree AGC=OFF.
|
||||||
|
At runtime, the MCU must read DIG_6 every frame to sync its outer-loop AGC.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_fpga_dig6_drives_agc_enable(self):
|
||||||
|
"""FPGA must drive gpio_dig6 from host_agc_enable, NOT tied low."""
|
||||||
|
rtl = (cp.FPGA_DIR / "radar_system_top.v").read_text()
|
||||||
|
# Must find: assign gpio_dig6 = host_agc_enable;
|
||||||
|
assert re.search(
|
||||||
|
r'assign\s+gpio_dig6\s*=\s*host_agc_enable\s*;', rtl
|
||||||
|
), "gpio_dig6 must be driven by host_agc_enable (not tied low)"
|
||||||
|
# Must NOT have the old tied-low pattern
|
||||||
|
assert not re.search(
|
||||||
|
r"assign\s+gpio_dig6\s*=\s*1'b0\s*;", rtl
|
||||||
|
), "gpio_dig6 must NOT be tied low — it carries AGC enable"
|
||||||
|
|
||||||
|
def test_fpga_agc_enable_boot_default_off(self):
|
||||||
|
"""FPGA host_agc_enable must reset to 0 (AGC off at boot)."""
|
||||||
|
v_defaults = cp.parse_verilog_reset_defaults()
|
||||||
|
assert "host_agc_enable" in v_defaults, (
|
||||||
|
"host_agc_enable not found in reset block"
|
||||||
|
)
|
||||||
|
assert v_defaults["host_agc_enable"] == 0, (
|
||||||
|
f"host_agc_enable reset default is {v_defaults['host_agc_enable']}, "
|
||||||
|
"expected 0 (AGC off at boot)"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_mcu_agc_constructor_default_off(self):
|
||||||
|
"""MCU ADAR1000_AGC constructor must default enabled=false."""
|
||||||
|
agc_cpp = (cp.MCU_LIB_DIR / "ADAR1000_AGC.cpp").read_text()
|
||||||
|
# The constructor initializer list must have enabled(false)
|
||||||
|
assert re.search(
|
||||||
|
r'enabled\s*\(\s*false\s*\)', agc_cpp
|
||||||
|
), "ADAR1000_AGC constructor must initialize enabled(false)"
|
||||||
|
assert not re.search(
|
||||||
|
r'enabled\s*\(\s*true\s*\)', agc_cpp
|
||||||
|
), "ADAR1000_AGC constructor must NOT initialize enabled(true)"
|
||||||
|
|
||||||
|
def test_mcu_reads_dig6_before_agc_gate(self):
|
||||||
|
"""MCU main loop must read DIG_6 GPIO to sync outerAgc.enabled."""
|
||||||
|
main_cpp = (cp.MCU_CODE_DIR / "main.cpp").read_text()
|
||||||
|
# DIG_6 must be read via HAL_GPIO_ReadPin
|
||||||
|
assert re.search(
|
||||||
|
r'HAL_GPIO_ReadPin\s*\(\s*FPGA_DIG6', main_cpp,
|
||||||
|
), "main.cpp must read DIG_6 GPIO via HAL_GPIO_ReadPin"
|
||||||
|
# outerAgc.enabled must be assigned from the DIG_6 reading
|
||||||
|
# (may be indirect via debounce variable like dig6_now)
|
||||||
|
assert re.search(
|
||||||
|
r'outerAgc\.enabled\s*=', main_cpp,
|
||||||
|
), "main.cpp must assign outerAgc.enabled from DIG_6 state"
|
||||||
|
|
||||||
|
def test_boot_invariant_all_layers_agc_off(self):
|
||||||
|
"""
|
||||||
|
At boot, all three layers must agree: AGC is OFF.
|
||||||
|
- FPGA: host_agc_enable resets to 0 -> DIG_6 low
|
||||||
|
- MCU: ADAR1000_AGC.enabled defaults to false
|
||||||
|
- GUI: reads status word 4 bit[11] = 0 -> reports MANUAL
|
||||||
|
"""
|
||||||
|
# FPGA
|
||||||
|
v_defaults = cp.parse_verilog_reset_defaults()
|
||||||
|
assert v_defaults.get("host_agc_enable") == 0
|
||||||
|
|
||||||
|
# MCU
|
||||||
|
agc_cpp = (cp.MCU_LIB_DIR / "ADAR1000_AGC.cpp").read_text()
|
||||||
|
assert re.search(r'enabled\s*\(\s*false\s*\)', agc_cpp)
|
||||||
|
|
||||||
|
# GUI: status word 4 bit[11] is host_agc_enable, which resets to 0.
|
||||||
|
# Verify the GUI parses bit[11] of status word 4 as the AGC flag.
|
||||||
|
gui_py = (cp.GUI_DIR / "radar_protocol.py").read_text()
|
||||||
|
assert re.search(
|
||||||
|
r'words\[4\].*>>\s*11|status_words\[4\].*>>\s*11',
|
||||||
|
gui_py,
|
||||||
|
), "GUI must parse AGC status from words[4] bit[11]"
|
||||||
|
|
||||||
|
def test_status_word4_agc_bit_matches_dig6_source(self):
|
||||||
|
"""
|
||||||
|
Status word 4 bit[11] and DIG_6 must both derive from host_agc_enable.
|
||||||
|
This guarantees the GUI status display can never lie about MCU AGC state.
|
||||||
|
"""
|
||||||
|
rtl = (cp.FPGA_DIR / "radar_system_top.v").read_text()
|
||||||
|
|
||||||
|
# DIG_6 driven by host_agc_enable
|
||||||
|
assert re.search(
|
||||||
|
r'assign\s+gpio_dig6\s*=\s*host_agc_enable\s*;', rtl
|
||||||
|
)
|
||||||
|
|
||||||
|
# Status word 4 must contain host_agc_enable (may be named
|
||||||
|
# status_agc_enable at the USB interface port boundary).
|
||||||
|
# Also verify the top-level wiring connects them.
|
||||||
|
usb_ft2232h = (cp.FPGA_DIR / "usb_data_interface_ft2232h.v").read_text()
|
||||||
|
usb_ft601 = (cp.FPGA_DIR / "usb_data_interface.v").read_text()
|
||||||
|
|
||||||
|
# USB interfaces use the port name status_agc_enable
|
||||||
|
found_in_ft2232h = "status_agc_enable" in usb_ft2232h
|
||||||
|
found_in_ft601 = "status_agc_enable" in usb_ft601
|
||||||
|
|
||||||
|
assert found_in_ft2232h or found_in_ft601, (
|
||||||
|
"status_agc_enable must appear in at least one USB interface's "
|
||||||
|
"status word to guarantee GUI status matches DIG_6"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify top-level wiring: status_agc_enable port is connected
|
||||||
|
# to host_agc_enable (same signal that drives DIG_6)
|
||||||
|
assert re.search(
|
||||||
|
r'\.status_agc_enable\s*\(\s*host_agc_enable\s*\)', rtl
|
||||||
|
), (
|
||||||
|
"Top-level must wire .status_agc_enable(host_agc_enable) "
|
||||||
|
"so status word and DIG_6 derive from the same signal"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_mcu_dig6_debounce_guards_enable_assignment(self):
|
||||||
|
"""
|
||||||
|
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 — defeating the debounce claim in the PR body.
|
||||||
|
"""
|
||||||
|
main_cpp = (cp.MCU_CODE_DIR / "main.cpp").read_text()
|
||||||
|
|
||||||
|
# (1) Current-frame DIG_6 sample must be captured in a local variable
|
||||||
|
# so it can be compared against the previous-frame value.
|
||||||
|
now_match = re.search(
|
||||||
|
r'(bool|int|uint8_t)\s+(\w*dig6\w*)\s*=\s*[^;]*?'
|
||||||
|
r'HAL_GPIO_ReadPin\s*\(\s*FPGA_DIG6[^;]*;',
|
||||||
|
main_cpp,
|
||||||
|
re.DOTALL,
|
||||||
|
)
|
||||||
|
assert now_match, (
|
||||||
|
"DIG_6 read must be stored in a local variable (e.g. `dig6_now`) "
|
||||||
|
"so the current sample can be compared against the previous frame"
|
||||||
|
)
|
||||||
|
now_var = now_match.group(2)
|
||||||
|
|
||||||
|
# (2) Previous-frame state must persist across iterations via static
|
||||||
|
# storage, and must default to false (matches FPGA boot: AGC off).
|
||||||
|
prev_match = re.search(
|
||||||
|
r'static\s+(bool|int|uint8_t)\s+(\w*dig6\w*)\s*=\s*(false|0)\s*;',
|
||||||
|
main_cpp,
|
||||||
|
)
|
||||||
|
assert prev_match, (
|
||||||
|
"A static previous-frame variable (e.g. "
|
||||||
|
"`static bool dig6_prev = false;`) must exist, initialized to "
|
||||||
|
"false so the debounce starts in sync with the FPGA boot default"
|
||||||
|
)
|
||||||
|
prev_var = prev_match.group(2)
|
||||||
|
assert prev_var != now_var, (
|
||||||
|
f"Current and previous DIG_6 variables must be distinct "
|
||||||
|
f"(both are '{now_var}')"
|
||||||
|
)
|
||||||
|
|
||||||
|
# (3) outerAgc.enabled assignment must be gated by now == prev.
|
||||||
|
guarded_assign = re.search(
|
||||||
|
rf'if\s*\(\s*{now_var}\s*==\s*{prev_var}\s*\)\s*\{{[^}}]*?'
|
||||||
|
rf'outerAgc\.enabled\s*=\s*{now_var}\s*;',
|
||||||
|
main_cpp,
|
||||||
|
re.DOTALL,
|
||||||
|
)
|
||||||
|
assert guarded_assign, (
|
||||||
|
f"`outerAgc.enabled = {now_var};` must be inside "
|
||||||
|
f"`if ({now_var} == {prev_var}) {{ ... }}` — the confirmation "
|
||||||
|
"guard that absorbs single-sample GPIO glitches. A naive "
|
||||||
|
"assignment without this guard reintroduces the glitch bug."
|
||||||
|
)
|
||||||
|
|
||||||
|
# (4) Previous-frame variable must advance each frame.
|
||||||
|
prev_update = re.search(
|
||||||
|
rf'{prev_var}\s*=\s*{now_var}\s*;',
|
||||||
|
main_cpp,
|
||||||
|
)
|
||||||
|
assert prev_update, (
|
||||||
|
f"`{prev_var} = {now_var};` must run each frame so the "
|
||||||
|
"debounce window slides forward; without it the guard is "
|
||||||
|
"stuck and enable changes never confirm"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# 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:
|
class TestTier1DataPacketLayout:
|
||||||
"""Verify data packet byte layout matches between Python and Verilog."""
|
"""Verify data packet byte layout matches between Python and Verilog."""
|
||||||
|
|
||||||
@@ -468,6 +1154,204 @@ class TestTier1STM32SettingsPacket:
|
|||||||
assert flag == [23, 46, 158, 237], f"Start flag: {flag}"
|
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
|
# TIER 2: Verilog Cosimulation
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
|||||||
@@ -68,13 +68,13 @@ The AERIS-10 main sub-systems are:
|
|||||||
- Clock Generator (AD9523-1)
|
- Clock Generator (AD9523-1)
|
||||||
- 2x Frequency Synthesizers (ADF4382)
|
- 2x Frequency Synthesizers (ADF4382)
|
||||||
- 4x 4-Channel Phase Shifters (ADAR1000) for RADAR pulse sequencing
|
- 4x 4-Channel Phase Shifters (ADAR1000) for RADAR pulse sequencing
|
||||||
- 2x ADS7830 ADCs (on Power Amplifier Boards) for Idq measurement
|
- 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 (on Power Amplifier Boards) for Vg control
|
- 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 for GUI map centering
|
- GPS module (UM982) for GUI map centering and per-detection position tagging
|
||||||
- GY-85 IMU for pitch/roll correction of target coordinates
|
- GY-85 IMU for pitch/roll correction of target coordinates
|
||||||
- BMP180 Barometer
|
- BMP180 Barometer
|
||||||
- Stepper Motor
|
- 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
|
- RF switches
|
||||||
|
|
||||||
- **16x Power Amplifier Boards** - Used only for AERIS-10E version, featuring 10Watt QPA2962 GaN amplifier for extended range
|
- **16x Power Amplifier Boards** - Used only for AERIS-10E version, featuring 10Watt QPA2962 GaN amplifier for extended range
|
||||||
|
|||||||
Reference in New Issue
Block a user