Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 76cfc71b19 | |||
| 161e9a66e4 | |||
| 7a35f42e61 | |||
| a03dd1329a | |||
| 6a11d33ef7 | |||
| b22cadb429 | |||
| f393e96d69 |
@@ -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(false)
|
, enabled(true)
|
||||||
, holdoff_counter(0)
|
, holdoff_counter(0)
|
||||||
, last_saturated(false)
|
, last_saturated(false)
|
||||||
, saturation_event_count(0)
|
, saturation_event_count(0)
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ extern "C" {
|
|||||||
* "BF" -- ADAR1000 beamformer
|
* "BF" -- ADAR1000 beamformer
|
||||||
* "PA" -- Power amplifier bias/monitoring
|
* "PA" -- Power amplifier bias/monitoring
|
||||||
* "FPGA" -- FPGA communication and handshake
|
* "FPGA" -- FPGA communication and handshake
|
||||||
* "USB" -- FT601 USB data path
|
* "USB" -- USB data path (FT2232H production / FT601 premium)
|
||||||
* "PWR" -- Power sequencing and rail monitoring
|
* "PWR" -- Power sequencing and rail monitoring
|
||||||
* "IMU" -- IMU/GPS/barometer sensors
|
* "IMU" -- IMU/GPS/barometer sensors
|
||||||
* "MOT" -- Stepper motor/scan mechanics
|
* "MOT" -- Stepper motor/scan mechanics
|
||||||
|
|||||||
@@ -46,9 +46,7 @@ extern "C" {
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include "stm32_spi.h"
|
#include "stm32_spi.h"
|
||||||
#include "stm32_delay.h"
|
#include "stm32_delay.h"
|
||||||
extern "C" {
|
#include "TinyGPSPlus.h"
|
||||||
#include "um982_gps.h"
|
|
||||||
}
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include "GY_85_HAL.h"
|
#include "GY_85_HAL.h"
|
||||||
}
|
}
|
||||||
@@ -123,8 +121,8 @@ UART_HandleTypeDef huart5;
|
|||||||
UART_HandleTypeDef huart3;
|
UART_HandleTypeDef huart3;
|
||||||
|
|
||||||
/* USER CODE BEGIN PV */
|
/* USER CODE BEGIN PV */
|
||||||
// UM982 dual-antenna GPS receiver
|
// The TinyGPSPlus object
|
||||||
UM982_GPS_t um982;
|
TinyGPSPlus gps;
|
||||||
|
|
||||||
// Global data structures
|
// Global data structures
|
||||||
GPS_Data_t current_gps_data = {0};
|
GPS_Data_t current_gps_data = {0};
|
||||||
@@ -175,7 +173,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; // [STM32-006] Legacy, unused -- kept for linker compat
|
extern uint8_t GUI_start_flag_received;
|
||||||
|
|
||||||
|
|
||||||
//RADAR
|
//RADAR
|
||||||
@@ -724,11 +722,14 @@ SystemError_t checkSystemHealth(void) {
|
|||||||
last_bmp_check = HAL_GetTick();
|
last_bmp_check = HAL_GetTick();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Check GPS Communication (30s grace period from boot / last valid fix)
|
// 6. Check GPS Communication
|
||||||
uint32_t gps_fix_age = um982_position_age(&um982);
|
static uint32_t last_gps_fix = 0;
|
||||||
if (gps_fix_age > 30000) {
|
if (gps.location.isUpdated()) {
|
||||||
|
last_gps_fix = HAL_GetTick();
|
||||||
|
}
|
||||||
|
if (HAL_GetTick() - last_gps_fix > 30000) {
|
||||||
current_error = ERROR_GPS_COMM;
|
current_error = ERROR_GPS_COMM;
|
||||||
DIAG_WARN("SYS", "Health check: GPS no fix for >30s (age=%lu ms)", (unsigned long)gps_fix_age);
|
DIAG_WARN("SYS", "Health check: GPS no fix for >30s");
|
||||||
return current_error;
|
return current_error;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1055,7 +1056,20 @@ static inline void delay_ms(uint32_t ms) { HAL_Delay(ms); }
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// smartDelay removed -- replaced by non-blocking um982_process() in main loop
|
// This custom version of delay() ensures that the gps object
|
||||||
|
// 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)
|
||||||
@@ -1199,14 +1213,7 @@ 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.
|
||||||
@@ -1595,12 +1602,6 @@ 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;
|
||||||
@@ -1776,34 +1777,10 @@ int main(void)
|
|||||||
//////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////
|
||||||
//////////////////////////////////////////GPS/////////////////////////////////////////
|
//////////////////////////////////////////GPS/////////////////////////////////////////
|
||||||
//////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////
|
||||||
DIAG_SECTION("GPS INIT (UM982)");
|
for(int i=0; i<10;i++){
|
||||||
DIAG("GPS", "Initializing UM982 on UART5 @ 115200 (baseline=50cm, tol=3cm)");
|
smartDelay(1000);
|
||||||
if (!um982_init(&um982, &huart5, 50.0f, 3.0f)) {
|
RADAR_Longitude = gps.location.lng();
|
||||||
DIAG_WARN("GPS", "UM982 init: no VERSIONA response -- module may need more time");
|
RADAR_Latitude = gps.location.lat();
|
||||||
// Not fatal: module may still start sending NMEA data after boot
|
|
||||||
} else {
|
|
||||||
DIAG("GPS", "UM982 init OK -- VERSIONA received");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect GPS data for a few seconds (non-blocking pump)
|
|
||||||
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°
|
//move Stepper to position 1 = 0°
|
||||||
@@ -1829,11 +1806,29 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* [STM32-006 FIXED] Removed blocking do-while loop that waited for
|
// Check if start flag was received and settings are ready
|
||||||
* usbHandler.isStartFlagReceived(). The production V7 PyQt GUI does not
|
do{
|
||||||
* send the legacy 4-byte start flag [23,46,158,237], so this loop hung
|
if (usbHandler.isStartFlagReceived() &&
|
||||||
* the MCU at boot indefinitely. The USB settings handshake (if ever
|
usbHandler.getState() == USBHandler::USBState::READY_FOR_DATA) {
|
||||||
* 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************/
|
||||||
@@ -2058,18 +2053,6 @@ 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//////////////////
|
||||||
//////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -2180,24 +2163,9 @@ int main(void)
|
|||||||
|
|
||||||
runRadarPulseSequence();
|
runRadarPulseSequence();
|
||||||
|
|
||||||
/* [AGC] Outer-loop AGC: sync enable from FPGA via DIG_6 (PD14),
|
/* [AGC] Outer-loop AGC: read FPGA saturation flag (DIG_5 / PD13),
|
||||||
* then read saturation flag (DIG_5 / PD13) and adjust ADAR1000 VGA
|
* adjust ADAR1000 VGA common gain once per radar frame (~258 ms).
|
||||||
* common gain once per radar frame (~258 ms).
|
* Only run when AGC is enabled — otherwise leave VGA gains untouched. */
|
||||||
* 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;
|
||||||
@@ -2635,7 +2603,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 = 115200;
|
huart5.Init.BaudRate = 9600;
|
||||||
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;
|
||||||
|
|||||||
@@ -1,586 +0,0 @@
|
|||||||
/*******************************************************************************
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
/*******************************************************************************
|
|
||||||
* 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 */
|
|
||||||
@@ -27,10 +27,6 @@ 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
|
||||||
|
|
||||||
@@ -78,10 +74,7 @@ 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
|
||||||
|
|
||||||
# GPS driver tests (need mocks + GPS source + -lm)
|
ALL_TESTS := $(TESTS_WITH_REAL) $(TESTS_MOCK_ONLY) $(TESTS_STANDALONE) $(TESTS_WITH_PLATFORM) $(TESTS_WITH_CXX)
|
||||||
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) \
|
||||||
@@ -200,20 +193,6 @@ 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
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ 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 };
|
||||||
|
|
||||||
@@ -35,26 +34,6 @@ 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 {
|
||||||
@@ -70,9 +49,6 @@ 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)
|
||||||
@@ -209,83 +185,6 @@ 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,7 +105,6 @@ 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 */
|
||||||
|
|
||||||
@@ -140,7 +139,6 @@ 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 {
|
||||||
@@ -189,23 +187,6 @@ 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 == false); // disabled by default — FPGA DIG_6 is source of truth
|
assert(agc.enabled == true);
|
||||||
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,7 +67,6 @@ 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
|
||||||
@@ -83,7 +82,6 @@ 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;
|
||||||
|
|
||||||
@@ -103,7 +101,6 @@ 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;
|
||||||
|
|
||||||
@@ -122,7 +119,6 @@ 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;
|
||||||
@@ -140,7 +136,6 @@ 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;
|
||||||
@@ -231,7 +226,6 @@ 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;
|
||||||
@@ -261,7 +255,6 @@ 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);
|
||||||
@@ -281,7 +274,6 @@ 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;
|
||||||
|
|||||||
@@ -1,853 +0,0 @@
|
|||||||
/*******************************************************************************
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
@@ -32,8 +32,8 @@ the `USB_MODE` parameter in `radar_system_top.v`:
|
|||||||
|
|
||||||
| USB_MODE | Interface | Bus Width | Speed | Board Target |
|
| USB_MODE | Interface | Bus Width | Speed | Board Target |
|
||||||
|----------|-----------|-----------|-------|--------------|
|
|----------|-----------|-----------|-------|--------------|
|
||||||
| 0 (default) | FT601 (USB 3.0) | 32-bit | 100 MHz | 200T premium dev board |
|
| 0 | FT601 (USB 3.0) | 32-bit | 100 MHz | 200T premium dev board |
|
||||||
| 1 | FT2232H (USB 2.0) | 8-bit | 60 MHz | 50T production board |
|
| 1 (default) | FT2232H (USB 2.0) | 8-bit | 60 MHz | 50T production board |
|
||||||
|
|
||||||
### How USB_MODE Works
|
### How USB_MODE Works
|
||||||
|
|
||||||
@@ -72,7 +72,8 @@ The parameter is set via a **wrapper module** that overrides the default:
|
|||||||
```
|
```
|
||||||
|
|
||||||
- **200T dev board**: `radar_system_top` is used directly as the top module.
|
- **200T dev board**: `radar_system_top` is used directly as the top module.
|
||||||
`USB_MODE` defaults to `0` (FT601). No wrapper needed.
|
`USB_MODE` defaults to `1` (FT2232H) since production is the primary target.
|
||||||
|
Override with `.USB_MODE(0)` for FT601 builds.
|
||||||
|
|
||||||
### RTL Files by USB Interface
|
### RTL Files by USB Interface
|
||||||
|
|
||||||
@@ -158,7 +159,7 @@ The build scripts automatically select the correct top module and constraints:
|
|||||||
|
|
||||||
You do NOT need to set `USB_MODE` manually. The top module selection handles it:
|
You do NOT need to set `USB_MODE` manually. The top module selection handles it:
|
||||||
- `radar_system_top_50t` forces `USB_MODE=1` internally
|
- `radar_system_top_50t` forces `USB_MODE=1` internally
|
||||||
- `radar_system_top` defaults to `USB_MODE=0`
|
- `radar_system_top` defaults to `USB_MODE=1` (FT2232H, production default)
|
||||||
|
|
||||||
## How to Select Constraints in Vivado
|
## How to Select Constraints in Vivado
|
||||||
|
|
||||||
@@ -190,9 +191,9 @@ read_xdc constraints/te0713_te0701_minimal.xdc
|
|||||||
| Target | Top module | USB_MODE | USB Interface | Notes |
|
| Target | Top module | USB_MODE | USB Interface | Notes |
|
||||||
|--------|------------|----------|---------------|-------|
|
|--------|------------|----------|---------------|-------|
|
||||||
| 50T Production (FTG256) | `radar_system_top_50t` | 1 | FT2232H (8-bit) | Wrapper sets USB_MODE=1, ties off FT601 |
|
| 50T Production (FTG256) | `radar_system_top_50t` | 1 | FT2232H (8-bit) | Wrapper sets USB_MODE=1, ties off FT601 |
|
||||||
| 200T Dev (FBG484) | `radar_system_top` | 0 (default) | FT601 (32-bit) | No wrapper needed |
|
| 200T Dev (FBG484) | `radar_system_top` | 0 (override) | FT601 (32-bit) | Build script overrides default USB_MODE=1 |
|
||||||
| Trenz TE0712/TE0701 | `radar_system_top_te0712_dev` | 0 (default) | FT601 (32-bit) | Minimal bring-up wrapper |
|
| Trenz TE0712/TE0701 | `radar_system_top_te0712_dev` | 0 (override) | FT601 (32-bit) | Minimal bring-up wrapper |
|
||||||
| Trenz TE0713/TE0701 | `radar_system_top_te0713_dev` | 0 (default) | FT601 (32-bit) | Alternate SoM wrapper |
|
| Trenz TE0713/TE0701 | `radar_system_top_te0713_dev` | 0 (override) | FT601 (32-bit) | Alternate SoM wrapper |
|
||||||
|
|
||||||
## Trenz Split Status
|
## Trenz Split Status
|
||||||
|
|
||||||
|
|||||||
@@ -70,9 +70,10 @@ set_input_jitter [get_clocks clk_100m] 0.1
|
|||||||
# NOTE: The physical DAC (U3, AD9708) receives its clock directly from the
|
# NOTE: The physical DAC (U3, AD9708) receives its clock directly from the
|
||||||
# AD9523 via a separate net (DAC_CLOCK), NOT from the FPGA. The FPGA
|
# AD9523 via a separate net (DAC_CLOCK), NOT from the FPGA. The FPGA
|
||||||
# uses this clock input for internal DAC data timing only. The RTL port
|
# uses this clock input for internal DAC data timing only. The RTL port
|
||||||
# `dac_clk` is an output that assigns clk_120m directly — it has no
|
# `dac_clk` is an RTL output that assigns clk_120m directly. It has no
|
||||||
# separate physical pin on this board and should be removed from the
|
# physical pin on the 50T board and is left unconnected here. The port
|
||||||
# RTL or left unconnected.
|
# CANNOT be removed from the RTL because the 200T board uses it with
|
||||||
|
# ODDR clock forwarding (pin H17, see xc7a200t_fbg484.xdc).
|
||||||
# FIX: Moved from C13 (IO_L12N = N-type) to D13 (IO_L12P = P-type MRCC).
|
# FIX: Moved from C13 (IO_L12N = N-type) to D13 (IO_L12P = P-type MRCC).
|
||||||
# Clock inputs must use the P-type pin of an MRCC pair (PLIO-9 DRC).
|
# Clock inputs must use the P-type pin of an MRCC pair (PLIO-9 DRC).
|
||||||
set_property PACKAGE_PIN D13 [get_ports {clk_120m_dac}]
|
set_property PACKAGE_PIN D13 [get_ports {clk_120m_dac}]
|
||||||
@@ -224,7 +225,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: AGC enable flag (PD14) — mirrors FPGA host_agc_enable to STM32
|
# DIG_6: reserved (PD14)
|
||||||
# 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}]
|
||||||
@@ -332,6 +333,44 @@ set_property DRIVE 8 [get_ports {ft_data[*]}]
|
|||||||
|
|
||||||
# ft_clkout constrained above in CLOCK CONSTRAINTS section (C4, 60 MHz)
|
# ft_clkout constrained above in CLOCK CONSTRAINTS section (C4, 60 MHz)
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# FT2232H Source-Synchronous Timing Constraints
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# FT2232H 245 Synchronous FIFO mode timing (60 MHz, period = 16.667 ns):
|
||||||
|
#
|
||||||
|
# FPGA Read Path (FT2232H drives data, FPGA samples):
|
||||||
|
# - Data valid before CLKOUT rising edge: t_vr(max) = 7.0 ns
|
||||||
|
# - Data hold after CLKOUT rising edge: t_hr(min) = 0.0 ns
|
||||||
|
# - Input delay max = period - t_vr = 16.667 - 7.0 = 9.667 ns
|
||||||
|
# - Input delay min = t_hr = 0.0 ns
|
||||||
|
#
|
||||||
|
# FPGA Write Path (FPGA drives data, FT2232H samples):
|
||||||
|
# - Data setup before next CLKOUT rising: t_su = 5.0 ns
|
||||||
|
# - Data hold after CLKOUT rising: t_hd = 0.0 ns
|
||||||
|
# - Output delay max = period - t_su = 16.667 - 5.0 = 11.667 ns
|
||||||
|
# - Output delay min = t_hd = 0.0 ns
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Input delays: FT2232H → FPGA (data bus and status signals)
|
||||||
|
set_input_delay -clock [get_clocks ft_clkout] -max 9.667 [get_ports {ft_data[*]}]
|
||||||
|
set_input_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_data[*]}]
|
||||||
|
set_input_delay -clock [get_clocks ft_clkout] -max 9.667 [get_ports {ft_rxf_n}]
|
||||||
|
set_input_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_rxf_n}]
|
||||||
|
set_input_delay -clock [get_clocks ft_clkout] -max 9.667 [get_ports {ft_txe_n}]
|
||||||
|
set_input_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_txe_n}]
|
||||||
|
|
||||||
|
# Output delays: FPGA → FT2232H (control strobes and data bus when writing)
|
||||||
|
set_output_delay -clock [get_clocks ft_clkout] -max 11.667 [get_ports {ft_data[*]}]
|
||||||
|
set_output_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_data[*]}]
|
||||||
|
set_output_delay -clock [get_clocks ft_clkout] -max 11.667 [get_ports {ft_rd_n}]
|
||||||
|
set_output_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_rd_n}]
|
||||||
|
set_output_delay -clock [get_clocks ft_clkout] -max 11.667 [get_ports {ft_wr_n}]
|
||||||
|
set_output_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_wr_n}]
|
||||||
|
set_output_delay -clock [get_clocks ft_clkout] -max 11.667 [get_ports {ft_oe_n}]
|
||||||
|
set_output_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_oe_n}]
|
||||||
|
set_output_delay -clock [get_clocks ft_clkout] -max 11.667 [get_ports {ft_siwu}]
|
||||||
|
set_output_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_siwu}]
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# STATUS / DEBUG OUTPUTS — NO PHYSICAL CONNECTIONS
|
# STATUS / DEBUG OUTPUTS — NO PHYSICAL CONNECTIONS
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -418,10 +457,10 @@ set_property BITSTREAM.CONFIG.UNUSEDPIN Pullup [current_design]
|
|||||||
# 4. JTAG: FPGA_TCK (L7), FPGA_TDI (N7), FPGA_TDO (N8), FPGA_TMS (M7).
|
# 4. JTAG: FPGA_TCK (L7), FPGA_TDI (N7), FPGA_TDO (N8), FPGA_TMS (M7).
|
||||||
# Dedicated pins — no XDC constraints needed.
|
# Dedicated pins — no XDC constraints needed.
|
||||||
#
|
#
|
||||||
# 5. dac_clk port: The RTL top module declares `dac_clk` as an output, but
|
# 5. dac_clk port: Not connected on the 50T board (DAC clocked directly from
|
||||||
# the physical board wires the DAC clock (AD9708 CLOCK pin) directly from
|
# AD9523). The RTL port exists for 200T board compatibility, where the FPGA
|
||||||
# the AD9523, not from the FPGA. This port should be removed from the RTL
|
# forwards the DAC clock via ODDR to pin H17 with generated clock and
|
||||||
# or left unconnected. It currently just assigns clk_120m_dac passthrough.
|
# timing constraints (see xc7a200t_fbg484.xdc). Do NOT remove from RTL.
|
||||||
#
|
#
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# END OF CONSTRAINTS
|
# END OF CONSTRAINTS
|
||||||
|
|||||||
@@ -11,10 +11,8 @@ 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 matched filter indexing)
|
// Chirp counter from transmitter (for frame sync and matched filter)
|
||||||
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,
|
||||||
@@ -394,31 +392,32 @@ mti_canceller #(
|
|||||||
.mti_first_chirp(mti_first_chirp)
|
.mti_first_chirp(mti_first_chirp)
|
||||||
);
|
);
|
||||||
|
|
||||||
// ========== FRAME SYNC FROM TRANSMITTER ==========
|
// ========== FRAME SYNC USING chirp_counter ==========
|
||||||
// [FPGA-001 FIXED] Use the authoritative new_chirp_frame signal from the
|
reg [5:0] chirp_counter_prev;
|
||||||
// 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
|
||||||
tx_frame_start_prev <= 1'b0;
|
chirp_counter_prev <= 6'd0;
|
||||||
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;
|
||||||
|
|
||||||
// Edge detect: tx_frame_start is a toggle-CDC derived pulse that
|
// Dynamic frame detection using host_chirps_per_elev.
|
||||||
// may be 1 clock wide. Capture rising edge for clean 1-cycle pulse.
|
// Detect frame boundary when chirp_counter changes AND is a
|
||||||
if (tx_frame_start && !tx_frame_start_prev) begin
|
// multiple of host_chirps_per_elev (0, N, 2N, 3N, ...).
|
||||||
new_frame_pulse <= 1'b1;
|
// Uses a modulo counter that resets at host_chirps_per_elev.
|
||||||
|
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
|
||||||
|
|
||||||
tx_frame_start_prev <= tx_frame_start;
|
// Store previous value
|
||||||
|
chirp_counter_prev <= chirp_counter;
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -484,6 +483,14 @@ 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): AGC enable flag (mirrors host_agc_enable)
|
output wire gpio_dig6, // DIG_6 (G12→PD14): reserved (tied low)
|
||||||
output wire gpio_dig7 // DIG_7 (H12→PD15): reserved (tied low)
|
output wire gpio_dig7 // DIG_7 (H12→PD15): reserved (tied low)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ module radar_system_top (
|
|||||||
parameter USE_LONG_CHIRP = 1'b1; // Default to long chirp
|
parameter USE_LONG_CHIRP = 1'b1; // Default to long chirp
|
||||||
parameter DOPPLER_ENABLE = 1'b1; // Enable Doppler processing
|
parameter DOPPLER_ENABLE = 1'b1; // Enable Doppler processing
|
||||||
parameter USB_ENABLE = 1'b1; // Enable USB data transfer
|
parameter USB_ENABLE = 1'b1; // Enable USB data transfer
|
||||||
parameter USB_MODE = 0; // 0=FT601 (32-bit, 200T), 1=FT2232H (8-bit, 50T)
|
parameter USB_MODE = 1; // 0=FT601 (32-bit, 200T), 1=FT2232H (8-bit, 50T production default)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// INTERNAL SIGNALS
|
// INTERNAL SIGNALS
|
||||||
@@ -505,8 +505,6 @@ 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),
|
||||||
@@ -1037,11 +1035,9 @@ 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: AGC enable flag — mirrors host_agc_enable so STM32 outer-loop AGC
|
// DIG_6, DIG_7: Reserved (tied low for future use).
|
||||||
// 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 = host_agc_enable;
|
assign gpio_dig6 = 1'b0;
|
||||||
assign gpio_dig7 = 1'b0;
|
assign gpio_dig7 = 1'b0;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -138,7 +138,12 @@ usb_data_interface usb_inst (
|
|||||||
.status_range_mode(2'b01),
|
.status_range_mode(2'b01),
|
||||||
.status_self_test_flags(5'b11111),
|
.status_self_test_flags(5'b11111),
|
||||||
.status_self_test_detail(8'hA5),
|
.status_self_test_detail(8'hA5),
|
||||||
.status_self_test_busy(1'b0)
|
.status_self_test_busy(1'b0),
|
||||||
|
// AGC status: tie off with benign defaults (no AGC on dev board)
|
||||||
|
.status_agc_current_gain(4'd0),
|
||||||
|
.status_agc_peak_magnitude(8'd0),
|
||||||
|
.status_agc_saturation_count(8'd0),
|
||||||
|
.status_agc_enable(1'b0)
|
||||||
);
|
);
|
||||||
|
|
||||||
endmodule
|
endmodule
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ PROD_RTL=(
|
|||||||
xfft_16.v
|
xfft_16.v
|
||||||
fft_engine.v
|
fft_engine.v
|
||||||
usb_data_interface.v
|
usb_data_interface.v
|
||||||
|
usb_data_interface_ft2232h.v
|
||||||
edge_detector.v
|
edge_detector.v
|
||||||
radar_mode_controller.v
|
radar_mode_controller.v
|
||||||
rx_gain_control.v
|
rx_gain_control.v
|
||||||
@@ -86,6 +87,33 @@ EXTRA_RTL=(
|
|||||||
frequency_matched_filter.v
|
frequency_matched_filter.v
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Shared RTL file lists for integration / system tests
|
||||||
|
# Centralised here so a new module only needs adding once.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Receiver chain (used by golden generate/compare tests)
|
||||||
|
RECEIVER_RTL=(
|
||||||
|
radar_receiver_final.v
|
||||||
|
radar_mode_controller.v
|
||||||
|
tb/ad9484_interface_400m_stub.v
|
||||||
|
ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v
|
||||||
|
cdc_modules.v fir_lowpass.v ddc_input_interface.v
|
||||||
|
chirp_memory_loader_param.v latency_buffer.v
|
||||||
|
matched_filter_multi_segment.v matched_filter_processing_chain.v
|
||||||
|
range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v
|
||||||
|
rx_gain_control.v mti_canceller.v
|
||||||
|
)
|
||||||
|
|
||||||
|
# Full system top (receiver chain + TX + USB + detection + self-test)
|
||||||
|
SYSTEM_RTL=(
|
||||||
|
radar_system_top.v
|
||||||
|
radar_transmitter.v dac_interface_single.v plfm_chirp_controller.v
|
||||||
|
"${RECEIVER_RTL[@]}"
|
||||||
|
usb_data_interface.v usb_data_interface_ft2232h.v edge_detector.v
|
||||||
|
cfar_ca.v fpga_self_test.v
|
||||||
|
)
|
||||||
|
|
||||||
# ---- Layer A: iverilog -Wall compilation ----
|
# ---- Layer A: iverilog -Wall compilation ----
|
||||||
run_lint_iverilog() {
|
run_lint_iverilog() {
|
||||||
local label="$1"
|
local label="$1"
|
||||||
@@ -219,26 +247,9 @@ run_lint_static() {
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# --- Single-line regex checks across all production RTL ---
|
# CHECK 5 ($readmemh in synth code) and CHECK 6 (unused includes)
|
||||||
for f in "$@"; do
|
# require multi-line ifdef tracking / cross-file analysis. Not feasible
|
||||||
[[ -f "$f" ]] || continue
|
# with line-by-line regex. Omitted — use Vivado lint instead.
|
||||||
case "$f" in tb/*) continue ;; esac
|
|
||||||
|
|
||||||
local linenum=0
|
|
||||||
while IFS= read -r line; do
|
|
||||||
linenum=$((linenum + 1))
|
|
||||||
|
|
||||||
# CHECK 5: $readmemh / $readmemb in synthesizable code
|
|
||||||
# (Only valid in simulation blocks — flag if outside `ifdef SIMULATION)
|
|
||||||
# This is hard to check line-by-line without tracking ifdefs.
|
|
||||||
# Skip for v1.
|
|
||||||
|
|
||||||
# CHECK 6: Unused `include files (informational only)
|
|
||||||
# Skip for v1.
|
|
||||||
|
|
||||||
: # placeholder — prevents empty loop body
|
|
||||||
done < "$f"
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "$err_count" -gt 0 ]]; then
|
if [[ "$err_count" -gt 0 ]]; then
|
||||||
echo -e "${RED}FAIL${NC} ($err_count errors, $warn_count warnings)"
|
echo -e "${RED}FAIL${NC} ($err_count errors, $warn_count warnings)"
|
||||||
@@ -420,57 +431,36 @@ if [[ "$QUICK" -eq 0 ]]; then
|
|||||||
run_test "Receiver (golden generate)" \
|
run_test "Receiver (golden generate)" \
|
||||||
tb/tb_rx_golden_reg.vvp \
|
tb/tb_rx_golden_reg.vvp \
|
||||||
-DGOLDEN_GENERATE \
|
-DGOLDEN_GENERATE \
|
||||||
tb/tb_radar_receiver_final.v radar_receiver_final.v \
|
tb/tb_radar_receiver_final.v "${RECEIVER_RTL[@]}"
|
||||||
radar_mode_controller.v tb/ad9484_interface_400m_stub.v \
|
|
||||||
ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v \
|
|
||||||
cdc_modules.v fir_lowpass.v ddc_input_interface.v \
|
|
||||||
chirp_memory_loader_param.v latency_buffer.v \
|
|
||||||
matched_filter_multi_segment.v matched_filter_processing_chain.v \
|
|
||||||
range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \
|
|
||||||
rx_gain_control.v mti_canceller.v
|
|
||||||
|
|
||||||
# Golden compare
|
# Golden compare
|
||||||
run_test "Receiver (golden compare)" \
|
run_test "Receiver (golden compare)" \
|
||||||
tb/tb_rx_compare_reg.vvp \
|
tb/tb_rx_compare_reg.vvp \
|
||||||
tb/tb_radar_receiver_final.v radar_receiver_final.v \
|
tb/tb_radar_receiver_final.v "${RECEIVER_RTL[@]}"
|
||||||
radar_mode_controller.v tb/ad9484_interface_400m_stub.v \
|
|
||||||
ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v \
|
|
||||||
cdc_modules.v fir_lowpass.v ddc_input_interface.v \
|
|
||||||
chirp_memory_loader_param.v latency_buffer.v \
|
|
||||||
matched_filter_multi_segment.v matched_filter_processing_chain.v \
|
|
||||||
range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \
|
|
||||||
rx_gain_control.v mti_canceller.v
|
|
||||||
|
|
||||||
# Full system top (monitoring-only, legacy)
|
# Full system top (monitoring-only, legacy)
|
||||||
run_test "System Top (radar_system_tb)" \
|
run_test "System Top (radar_system_tb)" \
|
||||||
tb/tb_system_reg.vvp \
|
tb/tb_system_reg.vvp \
|
||||||
tb/radar_system_tb.v radar_system_top.v \
|
tb/radar_system_tb.v "${SYSTEM_RTL[@]}"
|
||||||
radar_transmitter.v dac_interface_single.v plfm_chirp_controller.v \
|
|
||||||
radar_receiver_final.v tb/ad9484_interface_400m_stub.v \
|
|
||||||
ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v \
|
|
||||||
cdc_modules.v fir_lowpass.v ddc_input_interface.v \
|
|
||||||
chirp_memory_loader_param.v latency_buffer.v \
|
|
||||||
matched_filter_multi_segment.v matched_filter_processing_chain.v \
|
|
||||||
range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \
|
|
||||||
usb_data_interface.v edge_detector.v radar_mode_controller.v \
|
|
||||||
rx_gain_control.v cfar_ca.v mti_canceller.v fpga_self_test.v
|
|
||||||
|
|
||||||
# E2E integration (46 strict checks: TX, RX, USB R/W, CDC, safety, reset)
|
# E2E integration (46 strict checks: TX, RX, USB R/W, CDC, safety, reset)
|
||||||
run_test "System E2E (tb_system_e2e)" \
|
run_test "System E2E (tb_system_e2e)" \
|
||||||
tb/tb_system_e2e_reg.vvp \
|
tb/tb_system_e2e_reg.vvp \
|
||||||
tb/tb_system_e2e.v radar_system_top.v \
|
tb/tb_system_e2e.v "${SYSTEM_RTL[@]}"
|
||||||
radar_transmitter.v dac_interface_single.v plfm_chirp_controller.v \
|
|
||||||
radar_receiver_final.v tb/ad9484_interface_400m_stub.v \
|
# USB_MODE=1 (FT2232H production) variants of system tests
|
||||||
ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v \
|
run_test "System Top USB_MODE=1 (FT2232H)" \
|
||||||
cdc_modules.v fir_lowpass.v ddc_input_interface.v \
|
tb/tb_system_ft2232h_reg.vvp \
|
||||||
chirp_memory_loader_param.v latency_buffer.v \
|
-DUSB_MODE_1 \
|
||||||
matched_filter_multi_segment.v matched_filter_processing_chain.v \
|
tb/radar_system_tb.v "${SYSTEM_RTL[@]}"
|
||||||
range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \
|
|
||||||
usb_data_interface.v edge_detector.v radar_mode_controller.v \
|
run_test "System E2E USB_MODE=1 (FT2232H)" \
|
||||||
rx_gain_control.v cfar_ca.v mti_canceller.v fpga_self_test.v
|
tb/tb_system_e2e_ft2232h_reg.vvp \
|
||||||
|
-DUSB_MODE_1 \
|
||||||
|
tb/tb_system_e2e.v "${SYSTEM_RTL[@]}"
|
||||||
else
|
else
|
||||||
echo " (skipped receiver golden + system top + E2E — use without --quick)"
|
echo " (skipped receiver golden + system top + E2E — use without --quick)"
|
||||||
SKIP=$((SKIP + 4))
|
SKIP=$((SKIP + 6))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -108,6 +108,9 @@ add_files -fileset constrs_1 -norecurse [file join $project_root "constraints" "
|
|||||||
|
|
||||||
set_property top $top_module [current_fileset]
|
set_property top $top_module [current_fileset]
|
||||||
set_property verilog_define {FFT_XPM_BRAM} [current_fileset]
|
set_property verilog_define {FFT_XPM_BRAM} [current_fileset]
|
||||||
|
# Override USB_MODE to 0 (FT601) for 200T premium board.
|
||||||
|
# The RTL default is USB_MODE=1 (FT2232H, production 50T).
|
||||||
|
set_property generic {USB_MODE=0} [current_fileset]
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 2. Synthesis
|
# 2. Synthesis
|
||||||
|
|||||||
@@ -291,12 +291,9 @@ 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
|
||||||
Verilog '/' binds tighter than '-', so the division applies
|
= (adc_data << 9) - (0xFF << 8) [integer division]
|
||||||
only to the second concatenation:
|
= (adc_data << 9) - 0x7F80
|
||||||
{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
|
||||||
# Exact: (adc_val << 9) - 0xFF00, where 0xFF00 = {1'b0,8'hFF,9'b0}/2
|
# Simplified: center around zero, scale to 18-bit
|
||||||
adc_val = int(adc_samples[n])
|
adc_val = int(adc_samples[n])
|
||||||
adc_signed = (adc_val << 9) - 0xFF00 # Exact RTL: {1'b0,adc,9'b0} - {1'b0,8'hFF,9'b0}/2
|
adc_signed = (adc_val - 128) << 9 # Approximate RTL sign conversion to 18-bit
|
||||||
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)
|
||||||
|
|||||||
@@ -430,7 +430,13 @@ end
|
|||||||
// DUT INSTANTIATION
|
// DUT INSTANTIATION
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
radar_system_top dut (
|
radar_system_top #(
|
||||||
|
`ifdef USB_MODE_1
|
||||||
|
.USB_MODE(1) // FT2232H interface (production 50T board)
|
||||||
|
`else
|
||||||
|
.USB_MODE(0) // FT601 interface (200T dev board)
|
||||||
|
`endif
|
||||||
|
) dut (
|
||||||
// System Clocks
|
// System Clocks
|
||||||
.clk_100m(clk_100m),
|
.clk_100m(clk_100m),
|
||||||
.clk_120m_dac(clk_120m_dac),
|
.clk_120m_dac(clk_120m_dac),
|
||||||
@@ -619,7 +625,11 @@ initial begin
|
|||||||
// Optional: dump specific signals for debugging
|
// Optional: dump specific signals for debugging
|
||||||
$dumpvars(1, dut.tx_inst);
|
$dumpvars(1, dut.tx_inst);
|
||||||
$dumpvars(1, dut.rx_inst);
|
$dumpvars(1, dut.rx_inst);
|
||||||
|
`ifdef USB_MODE_1
|
||||||
|
$dumpvars(1, dut.gen_ft2232h.usb_inst);
|
||||||
|
`else
|
||||||
$dumpvars(1, dut.gen_ft601.usb_inst);
|
$dumpvars(1, dut.gen_ft601.usb_inst);
|
||||||
|
`endif
|
||||||
end
|
end
|
||||||
|
|
||||||
endmodule
|
endmodule
|
||||||
|
|||||||
@@ -96,31 +96,15 @@ 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
|
||||||
|
|
||||||
@@ -144,7 +128,6 @@ 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),
|
||||||
|
|||||||
@@ -382,7 +382,13 @@ end
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
// DUT INSTANTIATION
|
// DUT INSTANTIATION
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
radar_system_top dut (
|
radar_system_top #(
|
||||||
|
`ifdef USB_MODE_1
|
||||||
|
.USB_MODE(1) // FT2232H interface (production 50T board)
|
||||||
|
`else
|
||||||
|
.USB_MODE(0) // FT601 interface (200T dev board)
|
||||||
|
`endif
|
||||||
|
) dut (
|
||||||
.clk_100m(clk_100m),
|
.clk_100m(clk_100m),
|
||||||
.clk_120m_dac(clk_120m_dac),
|
.clk_120m_dac(clk_120m_dac),
|
||||||
.ft601_clk_in(ft601_clk_in),
|
.ft601_clk_in(ft601_clk_in),
|
||||||
@@ -554,10 +560,10 @@ initial begin
|
|||||||
do_reset;
|
do_reset;
|
||||||
|
|
||||||
// CRITICAL: Configure stream control to range-only BEFORE any chirps
|
// CRITICAL: Configure stream control to range-only BEFORE any chirps
|
||||||
// fire. The USB write FSM blocks on doppler_valid_ft if doppler stream
|
// fire. The USB write FSM gates on pending flags: if doppler stream is
|
||||||
// is enabled but no Doppler data arrives (needs 32 chirps/frame).
|
// enabled but no Doppler data arrives (needs 32 chirps/frame), the FSM
|
||||||
// Without this, the write FSM deadlocks and the read FSM can never
|
// stays in IDLE waiting for doppler_data_pending. With the write FSM
|
||||||
// activate (it requires write FSM == IDLE).
|
// not in IDLE, the read FSM cannot activate (bus arbitration rule).
|
||||||
bfm_send_cmd(8'h04, 8'h00, 16'h0001); // stream_control = range only
|
bfm_send_cmd(8'h04, 8'h00, 16'h0001); // stream_control = range only
|
||||||
// Wait for stream_control CDC to propagate (2-stage sync in ft601_clk)
|
// Wait for stream_control CDC to propagate (2-stage sync in ft601_clk)
|
||||||
// Must be long enough that stream_ctrl_sync_1 is updated before any
|
// Must be long enough that stream_ctrl_sync_1 is updated before any
|
||||||
@@ -778,7 +784,7 @@ initial begin
|
|||||||
|
|
||||||
// Restore defaults for subsequent tests
|
// Restore defaults for subsequent tests
|
||||||
bfm_send_cmd(8'h01, 8'h00, 16'h0001); // mode = auto-scan
|
bfm_send_cmd(8'h01, 8'h00, 16'h0001); // mode = auto-scan
|
||||||
bfm_send_cmd(8'h04, 8'h00, 16'h0001); // keep range-only (prevents write FSM deadlock)
|
bfm_send_cmd(8'h04, 8'h00, 16'h0001); // keep range-only (TB lacks 32-chirp doppler data)
|
||||||
bfm_send_cmd(8'h10, 8'h00, 16'd3000); // restore long chirp cycles
|
bfm_send_cmd(8'h10, 8'h00, 16'd3000); // restore long chirp cycles
|
||||||
|
|
||||||
$display("");
|
$display("");
|
||||||
@@ -913,7 +919,7 @@ initial begin
|
|||||||
// Need to re-send configuration since reset clears all registers
|
// Need to re-send configuration since reset clears all registers
|
||||||
stm32_mixers_enable = 1;
|
stm32_mixers_enable = 1;
|
||||||
ft601_txe = 0;
|
ft601_txe = 0;
|
||||||
bfm_send_cmd(8'h04, 8'h00, 16'h0001); // stream_control = range only (prevent deadlock)
|
bfm_send_cmd(8'h04, 8'h00, 16'h0001); // stream_control = range only (TB lacks doppler data)
|
||||||
#500; // Wait for stream_control CDC
|
#500; // Wait for stream_control CDC
|
||||||
bfm_send_cmd(8'h01, 8'h00, 16'h0001); // auto-scan
|
bfm_send_cmd(8'h01, 8'h00, 16'h0001); // auto-scan
|
||||||
bfm_send_cmd(8'h10, 8'h00, 16'd100); // short timing
|
bfm_send_cmd(8'h10, 8'h00, 16'd100); // short timing
|
||||||
@@ -947,7 +953,7 @@ initial begin
|
|||||||
check(dut.host_stream_control == 3'b000,
|
check(dut.host_stream_control == 3'b000,
|
||||||
"G10.2: All streams disabled (stream_control = 3'b000)");
|
"G10.2: All streams disabled (stream_control = 3'b000)");
|
||||||
|
|
||||||
// G10.3: Re-enable range only (keep range-only to prevent write FSM deadlock)
|
// G10.3: Re-enable range only (TB uses range-only — no doppler processing)
|
||||||
bfm_send_cmd(8'h04, 8'h00, 16'h0001); // stream_control = 3'b001
|
bfm_send_cmd(8'h04, 8'h00, 16'h0001); // stream_control = 3'b001
|
||||||
check(dut.host_stream_control == 3'b001,
|
check(dut.host_stream_control == 3'b001,
|
||||||
"G10.3: Range stream re-enabled (stream_control = 3'b001)");
|
"G10.3: Range stream re-enabled (stream_control = 3'b001)");
|
||||||
|
|||||||
@@ -6,15 +6,11 @@ module tb_usb_data_interface;
|
|||||||
localparam CLK_PERIOD = 10.0; // 100 MHz main clock
|
localparam CLK_PERIOD = 10.0; // 100 MHz main clock
|
||||||
localparam FT_CLK_PERIOD = 10.0; // 100 MHz FT601 clock (asynchronous)
|
localparam FT_CLK_PERIOD = 10.0; // 100 MHz FT601 clock (asynchronous)
|
||||||
|
|
||||||
// State definitions (mirror the DUT)
|
// State definitions (mirror the DUT — 4-state packed-word FSM)
|
||||||
localparam [2:0] S_IDLE = 3'd0,
|
localparam [3:0] S_IDLE = 4'd0,
|
||||||
S_SEND_HEADER = 3'd1,
|
S_SEND_DATA_WORD = 4'd1,
|
||||||
S_SEND_RANGE = 3'd2,
|
S_SEND_STATUS = 4'd2,
|
||||||
S_SEND_DOPPLER = 3'd3,
|
S_WAIT_ACK = 4'd3;
|
||||||
S_SEND_DETECT = 3'd4,
|
|
||||||
S_SEND_FOOTER = 3'd5,
|
|
||||||
S_WAIT_ACK = 3'd6,
|
|
||||||
S_SEND_STATUS = 3'd7; // Gap 2: status readback
|
|
||||||
|
|
||||||
// ── Signals ────────────────────────────────────────────────
|
// ── Signals ────────────────────────────────────────────────
|
||||||
reg clk;
|
reg clk;
|
||||||
@@ -219,9 +215,9 @@ module tb_usb_data_interface;
|
|||||||
end
|
end
|
||||||
endtask
|
endtask
|
||||||
|
|
||||||
// ── Helper: wait for DUT to reach a specific state ─────────
|
// ── Helper: wait for DUT to reach a specific write FSM state ──
|
||||||
task wait_for_state;
|
task wait_for_state;
|
||||||
input [2:0] target;
|
input [3:0] target;
|
||||||
input integer max_cyc;
|
input integer max_cyc;
|
||||||
integer cnt;
|
integer cnt;
|
||||||
begin
|
begin
|
||||||
@@ -280,7 +276,7 @@ module tb_usb_data_interface;
|
|||||||
// Set data_pending flags directly via hierarchical access.
|
// Set data_pending flags directly via hierarchical access.
|
||||||
// This is the standard TB technique for internal state setup —
|
// This is the standard TB technique for internal state setup —
|
||||||
// bypasses the CDC path for immediate, reliable flag setting.
|
// bypasses the CDC path for immediate, reliable flag setting.
|
||||||
// Call BEFORE assert_range_valid in tests that need SEND_DOPPLER/DETECT.
|
// Call BEFORE assert_range_valid in tests that need doppler/cfar data.
|
||||||
task preload_pending_data;
|
task preload_pending_data;
|
||||||
begin
|
begin
|
||||||
@(posedge ft601_clk_in);
|
@(posedge ft601_clk_in);
|
||||||
@@ -354,24 +350,26 @@ module tb_usb_data_interface;
|
|||||||
end
|
end
|
||||||
endtask
|
endtask
|
||||||
|
|
||||||
// Drive a complete packet through the FSM by sequentially providing
|
// Drive a complete data packet through the new 3-word packed FSM.
|
||||||
// range, doppler (4x), and cfar valid pulses.
|
// Pre-loads pending flags, triggers range_valid, and waits for IDLE.
|
||||||
|
// With the new FSM, all data is pre-packed in IDLE then sent as 3 words.
|
||||||
task drive_full_packet;
|
task drive_full_packet;
|
||||||
input [31:0] rng;
|
input [31:0] rng;
|
||||||
input [15:0] dr;
|
input [15:0] dr;
|
||||||
input [15:0] di;
|
input [15:0] di;
|
||||||
input det;
|
input det;
|
||||||
begin
|
begin
|
||||||
// Pre-load pending flags so FSM enters doppler/cfar states
|
// Set doppler/cfar captured values via CDC inputs
|
||||||
|
@(posedge clk);
|
||||||
|
doppler_real = dr;
|
||||||
|
doppler_imag = di;
|
||||||
|
cfar_detection = det;
|
||||||
|
@(posedge clk);
|
||||||
|
// Pre-load pending flags so FSM includes doppler/cfar in packet
|
||||||
preload_pending_data;
|
preload_pending_data;
|
||||||
|
// Trigger the packet
|
||||||
assert_range_valid(rng);
|
assert_range_valid(rng);
|
||||||
wait_for_state(S_SEND_DOPPLER, 100);
|
// Wait for complete packet cycle: IDLE → SEND_DATA_WORD(×3) → WAIT_ACK → IDLE
|
||||||
pulse_doppler_once(dr, di);
|
|
||||||
pulse_doppler_once(dr, di);
|
|
||||||
pulse_doppler_once(dr, di);
|
|
||||||
pulse_doppler_once(dr, di);
|
|
||||||
wait_for_state(S_SEND_DETECT, 100);
|
|
||||||
pulse_cfar_once(det);
|
|
||||||
wait_for_state(S_IDLE, 100);
|
wait_for_state(S_IDLE, 100);
|
||||||
end
|
end
|
||||||
endtask
|
endtask
|
||||||
@@ -414,101 +412,138 @@ module tb_usb_data_interface;
|
|||||||
"ft601_siwu_n=1 after reset");
|
"ft601_siwu_n=1 after reset");
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════
|
||||||
// TEST GROUP 2: Range data packet
|
// TEST GROUP 2: Data packet word packing
|
||||||
//
|
//
|
||||||
// Use backpressure to freeze the FSM at specific states
|
// New FSM packs 11-byte data into 3 × 32-bit words:
|
||||||
// so we can reliably sample outputs.
|
// Word 0: {HEADER, range[31:24], range[23:16], range[15:8]}
|
||||||
|
// Word 1: {range[7:0], dop_re_hi, dop_re_lo, dop_im_hi}
|
||||||
|
// Word 2: {dop_im_lo, detection, FOOTER, 0x00} BE=1110
|
||||||
|
//
|
||||||
|
// The DUT uses range_data_ready (1-cycle delayed range_valid_ft)
|
||||||
|
// to trigger packing. Doppler/CFAR _cap registers must be
|
||||||
|
// pre-loaded via hierarchical access because no valid pulse is
|
||||||
|
// given in this test (we only want to verify packing, not CDC).
|
||||||
// ════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════
|
||||||
$display("\n--- Test Group 2: Range Data Packet ---");
|
$display("\n--- Test Group 2: Data Packet Word Packing ---");
|
||||||
apply_reset;
|
apply_reset;
|
||||||
|
ft601_txe = 1; // Stall so we can inspect packed words
|
||||||
|
|
||||||
// Stall at SEND_HEADER so we can verify first range word later
|
// Set known doppler/cfar values on clk-domain inputs
|
||||||
ft601_txe = 1;
|
@(posedge clk);
|
||||||
|
doppler_real = 16'hABCD;
|
||||||
|
doppler_imag = 16'hEF01;
|
||||||
|
cfar_detection = 1'b1;
|
||||||
|
@(posedge clk);
|
||||||
|
|
||||||
|
// Pre-load pending flags AND captured-data registers directly.
|
||||||
|
// No doppler/cfar valid pulses are given, so the CDC capture path
|
||||||
|
// never fires — we must set the _cap registers via hierarchical
|
||||||
|
// access for the word-packing checks to be meaningful.
|
||||||
preload_pending_data;
|
preload_pending_data;
|
||||||
|
@(posedge ft601_clk_in);
|
||||||
|
uut.doppler_real_cap = 16'hABCD;
|
||||||
|
uut.doppler_imag_cap = 16'hEF01;
|
||||||
|
uut.cfar_detection_cap = 1'b1;
|
||||||
|
@(posedge ft601_clk_in);
|
||||||
|
|
||||||
assert_range_valid(32'hDEAD_BEEF);
|
assert_range_valid(32'hDEAD_BEEF);
|
||||||
wait_for_state(S_SEND_HEADER, 50);
|
|
||||||
repeat (2) @(posedge ft601_clk_in); #1;
|
|
||||||
check(uut.current_state === S_SEND_HEADER,
|
|
||||||
"Stalled in SEND_HEADER (backpressure)");
|
|
||||||
|
|
||||||
// Release: FSM drives header then moves to SEND_RANGE_DATA
|
// FSM should be in SEND_DATA_WORD, stalled on ft601_txe=1
|
||||||
|
wait_for_state(S_SEND_DATA_WORD, 50);
|
||||||
|
repeat (2) @(posedge ft601_clk_in); #1;
|
||||||
|
|
||||||
|
check(uut.current_state === S_SEND_DATA_WORD,
|
||||||
|
"Stalled in SEND_DATA_WORD (backpressure)");
|
||||||
|
|
||||||
|
// Verify pre-packed words
|
||||||
|
// range_profile = 0xDEAD_BEEF → range[31:24]=0xDE, [23:16]=0xAD, [15:8]=0xBE, [7:0]=0xEF
|
||||||
|
// Word 0: {0xAA, 0xDE, 0xAD, 0xBE}
|
||||||
|
check(uut.data_pkt_word0 === {8'hAA, 8'hDE, 8'hAD, 8'hBE},
|
||||||
|
"Word 0: {HEADER=AA, range[31:8]}");
|
||||||
|
// Word 1: {0xEF, 0xAB, 0xCD, 0xEF}
|
||||||
|
check(uut.data_pkt_word1 === {8'hEF, 8'hAB, 8'hCD, 8'hEF},
|
||||||
|
"Word 1: {range[7:0], dop_re, dop_im_hi}");
|
||||||
|
// Word 2: {0x01, detection_byte, 0x55, 0x00}
|
||||||
|
// detection_byte bit 7 = frame_start (sample_counter==0 → 1), bit 0 = cfar=1
|
||||||
|
// so detection_byte = 8'b1000_0001 = 8'h81
|
||||||
|
check(uut.data_pkt_word2 === {8'h01, 8'h81, 8'h55, 8'h00},
|
||||||
|
"Word 2: {dop_im_lo, det=81, FOOTER=55, pad=00}");
|
||||||
|
check(uut.data_pkt_be2 === 4'b1110,
|
||||||
|
"Word 2 BE=1110 (3 valid bytes + 1 pad)");
|
||||||
|
|
||||||
|
// Release backpressure and verify word 0 appears on bus.
|
||||||
|
// On the first posedge with !ft601_txe the FSM drives word 0 and
|
||||||
|
// advances data_word_idx 0→1 via NBA. After #1 the NBA has
|
||||||
|
// resolved, so we see idx=1 and ft601_data_out=word0.
|
||||||
ft601_txe = 0;
|
ft601_txe = 0;
|
||||||
@(posedge ft601_clk_in); #1;
|
@(posedge ft601_clk_in); #1;
|
||||||
// Now the FSM registered the header output and will transition
|
|
||||||
// At the NEXT posedge the state becomes SEND_RANGE_DATA
|
|
||||||
@(posedge ft601_clk_in); #1;
|
|
||||||
|
|
||||||
check(uut.current_state === S_SEND_RANGE,
|
|
||||||
"Entered SEND_RANGE_DATA after header");
|
|
||||||
|
|
||||||
// The first range word should be on the data bus (byte_counter=0 just
|
|
||||||
// drove range_profile_cap, byte_counter incremented to 1)
|
|
||||||
check(uut.ft601_data_out === 32'hDEAD_BEEF || uut.byte_counter <= 8'd1,
|
|
||||||
"Range data word 0 driven (range_profile_cap)");
|
|
||||||
|
|
||||||
|
check(uut.ft601_data_out === {8'hAA, 8'hDE, 8'hAD, 8'hBE},
|
||||||
|
"Word 0 driven on data bus after backpressure release");
|
||||||
check(ft601_wr_n === 1'b0,
|
check(ft601_wr_n === 1'b0,
|
||||||
"Write strobe active during range data");
|
"Write strobe active during SEND_DATA_WORD");
|
||||||
|
|
||||||
check(ft601_be === 4'b1111,
|
check(ft601_be === 4'b1111,
|
||||||
"Byte enable=1111 for range data");
|
"Byte enable=1111 for word 0");
|
||||||
|
check(uut.ft601_data_oe === 1'b1,
|
||||||
|
"Data bus output enabled during SEND_DATA_WORD");
|
||||||
|
|
||||||
// Wait for all 4 range words to complete
|
// Next posedge: FSM drives word 1, advances idx 1→2.
|
||||||
wait_for_state(S_SEND_DOPPLER, 50);
|
// After NBA: idx=2, ft601_data_out=word1.
|
||||||
#1;
|
@(posedge ft601_clk_in); #1;
|
||||||
check(uut.current_state === S_SEND_DOPPLER,
|
check(uut.data_word_idx === 2'd2,
|
||||||
"Advanced to SEND_DOPPLER_DATA after 4 range words");
|
"data_word_idx advanced past word 1 (now 2)");
|
||||||
|
check(uut.ft601_data_out === {8'hEF, 8'hAB, 8'hCD, 8'hEF},
|
||||||
|
"Word 1 driven on data bus");
|
||||||
|
check(ft601_be === 4'b1111,
|
||||||
|
"Byte enable=1111 for word 1");
|
||||||
|
|
||||||
|
// Next posedge: FSM drives word 2, idx resets 2→0,
|
||||||
|
// and current_state transitions to WAIT_ACK.
|
||||||
|
@(posedge ft601_clk_in); #1;
|
||||||
|
check(uut.current_state === S_WAIT_ACK,
|
||||||
|
"Transitioned to WAIT_ACK after 3 data words");
|
||||||
|
check(uut.ft601_data_out === {8'h01, 8'h81, 8'h55, 8'h00},
|
||||||
|
"Word 2 driven on data bus");
|
||||||
|
check(ft601_be === 4'b1110,
|
||||||
|
"Byte enable=1110 for word 2 (last byte is pad)");
|
||||||
|
|
||||||
|
// Then back to IDLE
|
||||||
|
@(posedge ft601_clk_in); #1;
|
||||||
|
check(uut.current_state === S_IDLE,
|
||||||
|
"Returned to IDLE after WAIT_ACK");
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════
|
||||||
// TEST GROUP 3: Header verification (stall to observe)
|
// TEST GROUP 3: Header and footer verification
|
||||||
// ════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════
|
||||||
$display("\n--- Test Group 3: Header Verification ---");
|
$display("\n--- Test Group 3: Header and Footer Verification ---");
|
||||||
apply_reset;
|
apply_reset;
|
||||||
ft601_txe = 1; // Stall at SEND_HEADER
|
ft601_txe = 1; // Stall to inspect
|
||||||
|
|
||||||
@(posedge clk);
|
@(posedge clk);
|
||||||
range_profile = 32'hCAFE_BABE;
|
doppler_real = 16'h0000;
|
||||||
range_valid = 1;
|
doppler_imag = 16'h0000;
|
||||||
repeat (4) @(posedge ft601_clk_in);
|
cfar_detection = 1'b0;
|
||||||
@(posedge clk);
|
@(posedge clk);
|
||||||
range_valid = 0;
|
preload_pending_data;
|
||||||
repeat (3) @(posedge ft601_clk_in);
|
assert_range_valid(32'hCAFE_BABE);
|
||||||
|
|
||||||
wait_for_state(S_SEND_HEADER, 50);
|
wait_for_state(S_SEND_DATA_WORD, 50);
|
||||||
repeat (2) @(posedge ft601_clk_in); #1;
|
repeat (2) @(posedge ft601_clk_in); #1;
|
||||||
|
|
||||||
check(uut.current_state === S_SEND_HEADER,
|
// Header is in byte 3 (MSB) of word 0
|
||||||
"Stalled in SEND_HEADER with backpressure");
|
check(uut.data_pkt_word0[31:24] === 8'hAA,
|
||||||
|
"Header byte 0xAA in word 0 MSB");
|
||||||
// Release backpressure - header will be latched at next posedge
|
// Footer is in byte 1 (bits [15:8]) of word 2
|
||||||
ft601_txe = 0;
|
check(uut.data_pkt_word2[15:8] === 8'h55,
|
||||||
@(posedge ft601_clk_in); #1;
|
"Footer byte 0x55 in word 2");
|
||||||
|
|
||||||
check(uut.ft601_data_out[7:0] === 8'hAA,
|
|
||||||
"Header byte 0xAA on data bus");
|
|
||||||
check(ft601_be === 4'b0001,
|
|
||||||
"Byte enable=0001 for header (lower byte only)");
|
|
||||||
check(ft601_wr_n === 1'b0,
|
|
||||||
"Write strobe active during header");
|
|
||||||
check(uut.ft601_data_oe === 1'b1,
|
|
||||||
"Data bus output enabled during header");
|
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════
|
||||||
// TEST GROUP 4: Doppler data verification
|
// TEST GROUP 4: Doppler data capture verification
|
||||||
// ════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════
|
||||||
$display("\n--- Test Group 4: Doppler Data Verification ---");
|
$display("\n--- Test Group 4: Doppler Data Capture ---");
|
||||||
apply_reset;
|
apply_reset;
|
||||||
ft601_txe = 0;
|
ft601_txe = 0;
|
||||||
|
|
||||||
// Preload only doppler pending (not cfar) so the FSM sends
|
|
||||||
// doppler data. After doppler, SEND_DETECT sees cfar_data_pending=0
|
|
||||||
// and skips to SEND_FOOTER, then WAIT_ACK, then IDLE.
|
|
||||||
preload_doppler_pending;
|
|
||||||
assert_range_valid(32'h0000_0001);
|
|
||||||
wait_for_state(S_SEND_DOPPLER, 100);
|
|
||||||
#1;
|
|
||||||
check(uut.current_state === S_SEND_DOPPLER,
|
|
||||||
"Reached SEND_DOPPLER_DATA");
|
|
||||||
|
|
||||||
// Provide doppler data via valid pulse (updates captured values)
|
// Provide doppler data via valid pulse (updates captured values)
|
||||||
@(posedge clk);
|
@(posedge clk);
|
||||||
doppler_real = 16'hAAAA;
|
doppler_real = 16'hAAAA;
|
||||||
@@ -524,110 +559,70 @@ module tb_usb_data_interface;
|
|||||||
check(uut.doppler_imag_cap === 16'h5555,
|
check(uut.doppler_imag_cap === 16'h5555,
|
||||||
"doppler_imag captured correctly");
|
"doppler_imag captured correctly");
|
||||||
|
|
||||||
// The FSM has doppler_data_pending set and sends 4 bytes, then
|
// Drive a packet with pending doppler + cfar (both needed for gating
|
||||||
// transitions past SEND_DETECT (cfar_data_pending=0) to IDLE.
|
// since all streams are enabled after reset/apply_reset).
|
||||||
|
preload_pending_data;
|
||||||
|
assert_range_valid(32'h0000_0001);
|
||||||
wait_for_state(S_IDLE, 100);
|
wait_for_state(S_IDLE, 100);
|
||||||
#1;
|
#1;
|
||||||
check(uut.current_state === S_IDLE,
|
check(uut.current_state === S_IDLE,
|
||||||
"Doppler done, packet completed");
|
"Packet completed with doppler data");
|
||||||
|
check(uut.doppler_data_pending === 1'b0,
|
||||||
|
"doppler_data_pending cleared after packet");
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════
|
||||||
// TEST GROUP 5: CFAR detection data
|
// TEST GROUP 5: CFAR detection data
|
||||||
// ════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════
|
||||||
$display("\n--- Test Group 5: CFAR Detection Data ---");
|
$display("\n--- Test Group 5: CFAR Detection Data ---");
|
||||||
// Start a new packet with both doppler and cfar pending to verify
|
|
||||||
// cfar data is properly sent in SEND_DETECTION_DATA.
|
|
||||||
apply_reset;
|
apply_reset;
|
||||||
ft601_txe = 0;
|
ft601_txe = 0;
|
||||||
preload_pending_data;
|
preload_pending_data;
|
||||||
assert_range_valid(32'h0000_0002);
|
assert_range_valid(32'h0000_0002);
|
||||||
// FSM races through: HEADER -> RANGE -> DOPPLER -> DETECT -> FOOTER -> IDLE
|
|
||||||
// All pending flags consumed proves SEND_DETECT was entered.
|
|
||||||
wait_for_state(S_IDLE, 200);
|
wait_for_state(S_IDLE, 200);
|
||||||
#1;
|
#1;
|
||||||
check(uut.cfar_data_pending === 1'b0,
|
check(uut.cfar_data_pending === 1'b0,
|
||||||
"Starting in SEND_DETECTION_DATA");
|
"cfar_data_pending cleared after packet");
|
||||||
|
|
||||||
// Verify the full packet completed with cfar data consumed
|
|
||||||
check(uut.current_state === S_IDLE &&
|
check(uut.current_state === S_IDLE &&
|
||||||
uut.doppler_data_pending === 1'b0 &&
|
uut.doppler_data_pending === 1'b0 &&
|
||||||
uut.cfar_data_pending === 1'b0,
|
uut.cfar_data_pending === 1'b0,
|
||||||
"CFAR detection sent, FSM advanced past SEND_DETECTION_DATA");
|
"CFAR detection sent, all pending flags cleared");
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════
|
||||||
// TEST GROUP 6: Footer check
|
// TEST GROUP 6: Footer retained after packet
|
||||||
//
|
|
||||||
// Strategy: drive packet with ft601_txe=0 all the way through.
|
|
||||||
// The SEND_FOOTER state is only active for 1 cycle, but we can
|
|
||||||
// poll the state machine at each ft601_clk_in edge to observe
|
|
||||||
// it. We use a monitor-style approach: run the packet and
|
|
||||||
// capture what ft601_data_out contains when we see SEND_FOOTER.
|
|
||||||
// ════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════
|
||||||
$display("\n--- Test Group 6: Footer Check ---");
|
$display("\n--- Test Group 6: Footer Retention ---");
|
||||||
apply_reset;
|
apply_reset;
|
||||||
ft601_txe = 0;
|
ft601_txe = 0;
|
||||||
|
|
||||||
// Drive packet through range data
|
@(posedge clk);
|
||||||
|
cfar_detection = 1'b1;
|
||||||
|
@(posedge clk);
|
||||||
preload_pending_data;
|
preload_pending_data;
|
||||||
assert_range_valid(32'hFACE_FEED);
|
assert_range_valid(32'hFACE_FEED);
|
||||||
wait_for_state(S_SEND_DOPPLER, 100);
|
|
||||||
// Feed doppler data (need 4 pulses)
|
|
||||||
pulse_doppler_once(16'h1111, 16'h2222);
|
|
||||||
pulse_doppler_once(16'h1111, 16'h2222);
|
|
||||||
pulse_doppler_once(16'h1111, 16'h2222);
|
|
||||||
pulse_doppler_once(16'h1111, 16'h2222);
|
|
||||||
wait_for_state(S_SEND_DETECT, 100);
|
|
||||||
// Feed cfar data, but keep ft601_txe=0 so it flows through
|
|
||||||
pulse_cfar_once(1'b1);
|
|
||||||
|
|
||||||
// Now the FSM should pass through SEND_FOOTER quickly.
|
|
||||||
// Use wait_for_state to reach SEND_FOOTER, or it may already
|
|
||||||
// be at WAIT_ACK/IDLE. Let's catch WAIT_ACK or IDLE.
|
|
||||||
// The footer values are latched into registers, so we can
|
|
||||||
// verify them even after the state transitions.
|
|
||||||
// Key verification: the FOOTER constant (0x55) must have been
|
|
||||||
// driven. We check this by looking at the constant definition.
|
|
||||||
// Since we can't easily freeze the FSM at SEND_FOOTER without
|
|
||||||
// also stalling SEND_DETECTION_DATA (both check ft601_txe),
|
|
||||||
// we verify the footer indirectly:
|
|
||||||
// 1. The packet completed (reached IDLE/WAIT_ACK)
|
|
||||||
// 2. ft601_data_out last held 0x55 during SEND_FOOTER
|
|
||||||
|
|
||||||
wait_for_state(S_IDLE, 100);
|
wait_for_state(S_IDLE, 100);
|
||||||
#1;
|
#1;
|
||||||
// If we reached IDLE, the full sequence ran including footer
|
|
||||||
check(uut.current_state === S_IDLE,
|
check(uut.current_state === S_IDLE,
|
||||||
"Full packet incl. footer completed, back in IDLE");
|
"Full packet incl. footer completed, back in IDLE");
|
||||||
|
|
||||||
// The registered ft601_data_out should still hold 0x55 from
|
// The last word driven was word 2 which contains footer 0x55.
|
||||||
// SEND_FOOTER (WAIT_ACK and IDLE don't overwrite ft601_data_out).
|
// WAIT_ACK and IDLE don't overwrite ft601_data_out, so it retains
|
||||||
// Actually, looking at the DUT: WAIT_ACK only sets wr_n=1 and
|
// the last driven value.
|
||||||
// data_oe=0, it doesn't change ft601_data_out. So it retains 0x55.
|
check(uut.ft601_data_out[15:8] === 8'h55,
|
||||||
check(uut.ft601_data_out[7:0] === 8'h55,
|
"ft601_data_out retains footer 0x55 in word 2 position");
|
||||||
"ft601_data_out retains footer 0x55 after packet");
|
|
||||||
|
|
||||||
// Verify WAIT_ACK behavior by doing another packet and catching it
|
// Verify WAIT_ACK → IDLE transition
|
||||||
apply_reset;
|
apply_reset;
|
||||||
ft601_txe = 0;
|
ft601_txe = 0;
|
||||||
preload_pending_data;
|
preload_pending_data;
|
||||||
assert_range_valid(32'h1234_5678);
|
assert_range_valid(32'h1234_5678);
|
||||||
wait_for_state(S_SEND_DOPPLER, 100);
|
|
||||||
pulse_doppler_once(16'hABCD, 16'hEF01);
|
|
||||||
pulse_doppler_once(16'hABCD, 16'hEF01);
|
|
||||||
pulse_doppler_once(16'hABCD, 16'hEF01);
|
|
||||||
pulse_doppler_once(16'hABCD, 16'hEF01);
|
|
||||||
wait_for_state(S_SEND_DETECT, 100);
|
|
||||||
pulse_cfar_once(1'b0);
|
|
||||||
// WAIT_ACK lasts exactly 1 ft601_clk_in cycle then goes IDLE.
|
|
||||||
// Poll for IDLE (which means WAIT_ACK already happened).
|
|
||||||
wait_for_state(S_IDLE, 100);
|
wait_for_state(S_IDLE, 100);
|
||||||
#1;
|
#1;
|
||||||
check(uut.current_state === S_IDLE,
|
check(uut.current_state === S_IDLE,
|
||||||
"Returned to IDLE after WAIT_ACK");
|
"Returned to IDLE after WAIT_ACK");
|
||||||
check(ft601_wr_n === 1'b1,
|
check(ft601_wr_n === 1'b1,
|
||||||
"ft601_wr_n deasserted in IDLE (was deasserted in WAIT_ACK)");
|
"ft601_wr_n deasserted in IDLE");
|
||||||
check(uut.ft601_data_oe === 1'b0,
|
check(uut.ft601_data_oe === 1'b0,
|
||||||
"Data bus released in IDLE (was released in WAIT_ACK)");
|
"Data bus released in IDLE");
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════
|
||||||
// TEST GROUP 7: Full packet sequence (end-to-end)
|
// TEST GROUP 7: Full packet sequence (end-to-end)
|
||||||
@@ -646,23 +641,24 @@ module tb_usb_data_interface;
|
|||||||
// ════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════
|
||||||
$display("\n--- Test Group 8: FIFO Backpressure ---");
|
$display("\n--- Test Group 8: FIFO Backpressure ---");
|
||||||
apply_reset;
|
apply_reset;
|
||||||
ft601_txe = 1;
|
ft601_txe = 1; // FIFO full — stall
|
||||||
|
|
||||||
|
preload_pending_data;
|
||||||
assert_range_valid(32'hBBBB_CCCC);
|
assert_range_valid(32'hBBBB_CCCC);
|
||||||
|
|
||||||
wait_for_state(S_SEND_HEADER, 50);
|
wait_for_state(S_SEND_DATA_WORD, 50);
|
||||||
repeat (10) @(posedge ft601_clk_in); #1;
|
repeat (10) @(posedge ft601_clk_in); #1;
|
||||||
|
|
||||||
check(uut.current_state === S_SEND_HEADER,
|
check(uut.current_state === S_SEND_DATA_WORD,
|
||||||
"Stalled in SEND_HEADER when ft601_txe=1 (FIFO full)");
|
"Stalled in SEND_DATA_WORD when ft601_txe=1 (FIFO full)");
|
||||||
check(ft601_wr_n === 1'b1,
|
check(ft601_wr_n === 1'b1,
|
||||||
"ft601_wr_n not asserted during backpressure stall");
|
"ft601_wr_n not asserted during backpressure stall");
|
||||||
|
|
||||||
ft601_txe = 0;
|
ft601_txe = 0;
|
||||||
repeat (2) @(posedge ft601_clk_in); #1;
|
repeat (6) @(posedge ft601_clk_in); #1;
|
||||||
|
|
||||||
check(uut.current_state !== S_SEND_HEADER,
|
check(uut.current_state === S_IDLE || uut.current_state === S_WAIT_ACK,
|
||||||
"Resumed from SEND_HEADER after backpressure released");
|
"Resumed and completed after backpressure released");
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════
|
||||||
// TEST GROUP 9: Clock divider
|
// TEST GROUP 9: Clock divider
|
||||||
@@ -705,13 +701,6 @@ module tb_usb_data_interface;
|
|||||||
ft601_txe = 0;
|
ft601_txe = 0;
|
||||||
preload_pending_data;
|
preload_pending_data;
|
||||||
assert_range_valid(32'h1111_2222);
|
assert_range_valid(32'h1111_2222);
|
||||||
wait_for_state(S_SEND_DOPPLER, 100);
|
|
||||||
pulse_doppler_once(16'h3333, 16'h4444);
|
|
||||||
pulse_doppler_once(16'h3333, 16'h4444);
|
|
||||||
pulse_doppler_once(16'h3333, 16'h4444);
|
|
||||||
pulse_doppler_once(16'h3333, 16'h4444);
|
|
||||||
wait_for_state(S_SEND_DETECT, 100);
|
|
||||||
pulse_cfar_once(1'b0);
|
|
||||||
wait_for_state(S_WAIT_ACK, 50);
|
wait_for_state(S_WAIT_ACK, 50);
|
||||||
#1;
|
#1;
|
||||||
|
|
||||||
@@ -805,7 +794,7 @@ module tb_usb_data_interface;
|
|||||||
// Start a write packet
|
// Start a write packet
|
||||||
preload_pending_data;
|
preload_pending_data;
|
||||||
assert_range_valid(32'hFACE_FEED);
|
assert_range_valid(32'hFACE_FEED);
|
||||||
wait_for_state(S_SEND_HEADER, 50);
|
wait_for_state(S_SEND_DATA_WORD, 50);
|
||||||
@(posedge ft601_clk_in); #1;
|
@(posedge ft601_clk_in); #1;
|
||||||
|
|
||||||
// While write FSM is active, assert RXF=0 (host has data)
|
// While write FSM is active, assert RXF=0 (host has data)
|
||||||
@@ -818,13 +807,6 @@ module tb_usb_data_interface;
|
|||||||
|
|
||||||
// Deassert RXF, complete the write packet
|
// Deassert RXF, complete the write packet
|
||||||
ft601_rxf = 1;
|
ft601_rxf = 1;
|
||||||
wait_for_state(S_SEND_DOPPLER, 100);
|
|
||||||
pulse_doppler_once(16'hAAAA, 16'hBBBB);
|
|
||||||
pulse_doppler_once(16'hAAAA, 16'hBBBB);
|
|
||||||
pulse_doppler_once(16'hAAAA, 16'hBBBB);
|
|
||||||
pulse_doppler_once(16'hAAAA, 16'hBBBB);
|
|
||||||
wait_for_state(S_SEND_DETECT, 100);
|
|
||||||
pulse_cfar_once(1'b1);
|
|
||||||
wait_for_state(S_IDLE, 100);
|
wait_for_state(S_IDLE, 100);
|
||||||
@(posedge ft601_clk_in); #1;
|
@(posedge ft601_clk_in); #1;
|
||||||
|
|
||||||
@@ -841,32 +823,42 @@ module tb_usb_data_interface;
|
|||||||
// ════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════
|
||||||
// TEST GROUP 15: Stream Control Gating (Gap 2)
|
// TEST GROUP 15: Stream Control Gating (Gap 2)
|
||||||
// Verify that disabling individual streams causes the write
|
// Verify that disabling individual streams causes the write
|
||||||
// FSM to skip those data phases.
|
// FSM to zero those fields in the packed words.
|
||||||
// ════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════
|
||||||
$display("\n--- Test Group 15: Stream Control Gating (Gap 2) ---");
|
$display("\n--- Test Group 15: Stream Control Gating (Gap 2) ---");
|
||||||
|
|
||||||
// 15a: Disable doppler stream (stream_control = 3'b101 = range + cfar only)
|
// 15a: Disable doppler stream (stream_control = 3'b101 = range + cfar only)
|
||||||
apply_reset;
|
apply_reset;
|
||||||
ft601_txe = 0;
|
ft601_txe = 1; // Stall to inspect packed words
|
||||||
stream_control = 3'b101; // range + cfar, no doppler
|
stream_control = 3'b101; // range + cfar, no doppler
|
||||||
// Wait for CDC propagation (2-stage sync)
|
// Wait for CDC propagation (2-stage sync)
|
||||||
repeat (6) @(posedge ft601_clk_in);
|
repeat (6) @(posedge ft601_clk_in);
|
||||||
|
|
||||||
// Preload cfar pending so the FSM enters the SEND_DETECT data path
|
@(posedge clk);
|
||||||
// (without it, SEND_DETECT skips immediately on !cfar_data_pending).
|
doppler_real = 16'hAAAA;
|
||||||
preload_cfar_pending;
|
doppler_imag = 16'hBBBB;
|
||||||
// Drive range valid — triggers write FSM
|
cfar_detection = 1'b1;
|
||||||
assert_range_valid(32'hAA11_BB22);
|
@(posedge clk);
|
||||||
// FSM: IDLE -> SEND_HEADER -> SEND_RANGE (doppler disabled) -> SEND_DETECT -> FOOTER
|
|
||||||
// The FSM races through SEND_DETECT in 1 cycle (cfar_data_pending is consumed).
|
|
||||||
// Verify the packet completed correctly (doppler was skipped).
|
|
||||||
wait_for_state(S_IDLE, 200);
|
|
||||||
#1;
|
|
||||||
// Reaching IDLE proves: HEADER -> RANGE -> (skip DOPPLER) -> DETECT -> FOOTER -> ACK -> IDLE.
|
|
||||||
// cfar_data_pending consumed confirms SEND_DETECT was entered.
|
|
||||||
check(uut.current_state === S_IDLE && uut.cfar_data_pending === 1'b0,
|
|
||||||
"Stream gate: reached SEND_DETECT (range sent, doppler skipped)");
|
|
||||||
|
|
||||||
|
preload_cfar_pending;
|
||||||
|
assert_range_valid(32'hAA11_BB22);
|
||||||
|
|
||||||
|
wait_for_state(S_SEND_DATA_WORD, 200);
|
||||||
|
repeat (2) @(posedge ft601_clk_in); #1;
|
||||||
|
|
||||||
|
// With doppler disabled, doppler fields in words 1 and 2 should be zero
|
||||||
|
// Word 1: {range[7:0], 0x00, 0x00, 0x00} (doppler zeroed)
|
||||||
|
check(uut.data_pkt_word1[23:0] === 24'h000000,
|
||||||
|
"Stream gate: doppler bytes zeroed in word 1 when disabled");
|
||||||
|
|
||||||
|
// Word 2 byte 3 (dop_im_lo) should also be zero
|
||||||
|
check(uut.data_pkt_word2[31:24] === 8'h00,
|
||||||
|
"Stream gate: dop_im_lo zeroed in word 2 when disabled");
|
||||||
|
|
||||||
|
// Let it complete
|
||||||
|
ft601_txe = 0;
|
||||||
|
wait_for_state(S_IDLE, 100);
|
||||||
|
#1;
|
||||||
check(uut.current_state === S_IDLE,
|
check(uut.current_state === S_IDLE,
|
||||||
"Stream gate: packet completed without doppler");
|
"Stream gate: packet completed without doppler");
|
||||||
|
|
||||||
@@ -951,28 +943,6 @@ module tb_usb_data_interface;
|
|||||||
"Status readback: returned to IDLE after 8-word response");
|
"Status readback: returned to IDLE after 8-word response");
|
||||||
|
|
||||||
// Verify the status snapshot was captured correctly.
|
// Verify the status snapshot was captured correctly.
|
||||||
// status_words[0] = {0xFF, 3'b000, mode[1:0], 5'b0, stream_ctrl[2:0], cfar_threshold[15:0]}
|
|
||||||
// = {8'hFF, 3'b000, 2'b01, 5'b00000, 3'b101, 16'hABCD}
|
|
||||||
// = 0xFF_09_05_ABCD... let's compute:
|
|
||||||
// Byte 3: 0xFF = 8'hFF
|
|
||||||
// Byte 2: {3'b000, 2'b01} = 5'b00001 + 3 high bits of next field...
|
|
||||||
// Actually the packing is: {8'hFF, 3'b000, status_radar_mode[1:0], 5'b00000, status_stream_ctrl[2:0], status_cfar_threshold[15:0]}
|
|
||||||
// = {8'hFF, 3'b000, 2'b01, 5'b00000, 3'b101, 16'hABCD}
|
|
||||||
// = 8'hFF, 5'b00001, 8'b00000101, 16'hABCD
|
|
||||||
// = FF_09_05_ABCD? Let me compute carefully:
|
|
||||||
// Bits [31:24] = 8'hFF = 0xFF
|
|
||||||
// Bits [23:21] = 3'b000
|
|
||||||
// Bits [20:19] = 2'b01 (mode)
|
|
||||||
// Bits [18:14] = 5'b00000
|
|
||||||
// Bits [13:11] = 3'b101 (stream_ctrl)
|
|
||||||
// Bits [10:0] = ... wait, cfar_threshold is 16 bits → [15:0]
|
|
||||||
// Total bits = 8+3+2+5+3+16 = 37 bits — won't fit in 32!
|
|
||||||
// Re-reading the RTL: the packing at line 241 is:
|
|
||||||
// {8'hFF, 3'b000, status_radar_mode, 5'b00000, status_stream_ctrl, status_cfar_threshold}
|
|
||||||
// = 8 + 3 + 2 + 5 + 3 + 16 = 37 bits
|
|
||||||
// This would be truncated to 32 bits. Let me re-read the actual RTL to check.
|
|
||||||
// For now, just verify status_words[1] (word index 1 in the packet = idx 2 in FSM)
|
|
||||||
// status_words[1] = {status_long_chirp, status_long_listen} = {16'd3000, 16'd13700}
|
|
||||||
check(uut.status_words[1] === {16'd3000, 16'd13700},
|
check(uut.status_words[1] === {16'd3000, 16'd13700},
|
||||||
"Status readback: word 1 = {long_chirp, long_listen}");
|
"Status readback: word 1 = {long_chirp, long_listen}");
|
||||||
check(uut.status_words[2] === {16'd17540, 16'd50},
|
check(uut.status_words[2] === {16'd17540, 16'd50},
|
||||||
|
|||||||
@@ -1,3 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* usb_data_interface.v
|
||||||
|
*
|
||||||
|
* FT601 USB 3.0 SuperSpeed FIFO Interface (32-bit bus, 100 MHz ft601_clk).
|
||||||
|
* Used on the 200T premium dev board. Production 50T board uses
|
||||||
|
* usb_data_interface_ft2232h.v (FT2232H, 8-bit, 60 MHz) instead.
|
||||||
|
*
|
||||||
|
* USB disconnect recovery:
|
||||||
|
* A clock-activity watchdog in the clk domain detects when ft601_clk_in
|
||||||
|
* stops (USB cable unplugged). After ~0.65 ms of silence (65536 system
|
||||||
|
* clocks) it asserts ft601_clk_lost, which is OR'd into the FT-domain
|
||||||
|
* reset so FSMs and FIFOs return to a clean state. When ft601_clk_in
|
||||||
|
* resumes, a 2-stage reset synchronizer deasserts the reset cleanly.
|
||||||
|
*/
|
||||||
module usb_data_interface (
|
module usb_data_interface (
|
||||||
input wire clk, // Main clock (100MHz recommended)
|
input wire clk, // Main clock (100MHz recommended)
|
||||||
input wire reset_n,
|
input wire reset_n,
|
||||||
@@ -15,13 +29,18 @@ module usb_data_interface (
|
|||||||
// FT601 Interface (Slave FIFO mode)
|
// FT601 Interface (Slave FIFO mode)
|
||||||
// Data bus
|
// Data bus
|
||||||
inout wire [31:0] ft601_data, // 32-bit bidirectional data bus
|
inout wire [31:0] ft601_data, // 32-bit bidirectional data bus
|
||||||
output reg [3:0] ft601_be, // Byte enable (4 lanes for 32-bit mode)
|
output reg [3:0] ft601_be, // Byte enable (active-HIGH per DS_FT600Q-FT601Q Table 3.2)
|
||||||
|
|
||||||
// Control signals
|
// Control signals
|
||||||
output reg ft601_txe_n, // Transmit enable (active low)
|
// VESTIGIAL OUTPUTS — kept for 200T board port compatibility.
|
||||||
output reg ft601_rxf_n, // Receive enable (active low)
|
// On the 200T, these are constrained to physical pins G21 (TXE) and
|
||||||
input wire ft601_txe, // TXE: Transmit FIFO Not Full (high = space available to write)
|
// G22 (RXF) in xc7a200t_fbg484.xdc. Removing them from the RTL would
|
||||||
input wire ft601_rxf, // RXF: Receive FIFO Not Empty (high = data available to read)
|
// break the 200T build. They are reset to 1 and never driven; the
|
||||||
|
// actual FT601 flow-control inputs are ft601_txe and ft601_rxf below.
|
||||||
|
output reg ft601_txe_n, // VESTIGIAL: unused output, always 1
|
||||||
|
output reg ft601_rxf_n, // VESTIGIAL: unused output, always 1
|
||||||
|
input wire ft601_txe, // TXE: Transmit FIFO Not Full (active-low: 0 = space available)
|
||||||
|
input wire ft601_rxf, // RXF: Receive FIFO Not Empty (active-low: 0 = data available)
|
||||||
output reg ft601_wr_n, // Write strobe (active low)
|
output reg ft601_wr_n, // Write strobe (active low)
|
||||||
output reg ft601_rd_n, // Read strobe (active low)
|
output reg ft601_rd_n, // Read strobe (active low)
|
||||||
output reg ft601_oe_n, // Output enable (active low)
|
output reg ft601_oe_n, // Output enable (active low)
|
||||||
@@ -97,21 +116,26 @@ localparam FT601_BURST_SIZE = 512; // Max burst size in bytes
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
// WRITE FSM State definitions (Verilog-2001 compatible)
|
// WRITE FSM State definitions (Verilog-2001 compatible)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
localparam [2:0] IDLE = 3'd0,
|
// Rewritten: data packet is now 3 x 32-bit writes (11 payload bytes + 1 pad).
|
||||||
SEND_HEADER = 3'd1,
|
// Word 0: {HEADER, range[31:24], range[23:16], range[15:8]} BE=1111
|
||||||
SEND_RANGE_DATA = 3'd2,
|
// Word 1: {range[7:0], doppler_real[15:8], doppler_real[7:0], doppler_imag[15:8]} BE=1111
|
||||||
SEND_DOPPLER_DATA = 3'd3,
|
// Word 2: {doppler_imag[7:0], detection, FOOTER, 8'h00} BE=1110
|
||||||
SEND_DETECTION_DATA = 3'd4,
|
localparam [3:0] IDLE = 4'd0,
|
||||||
SEND_FOOTER = 3'd5,
|
SEND_DATA_WORD = 4'd1,
|
||||||
WAIT_ACK = 3'd6,
|
SEND_STATUS = 4'd2,
|
||||||
SEND_STATUS = 3'd7; // Gap 2: status readback
|
WAIT_ACK = 4'd3;
|
||||||
|
|
||||||
reg [2:0] current_state;
|
reg [3:0] current_state;
|
||||||
reg [7:0] byte_counter;
|
reg [1:0] data_word_idx; // 0..2 for 3-word data packet
|
||||||
reg [31:0] data_buffer;
|
|
||||||
reg [31:0] ft601_data_out;
|
reg [31:0] ft601_data_out;
|
||||||
reg ft601_data_oe; // Output enable for bidirectional data bus
|
reg ft601_data_oe; // Output enable for bidirectional data bus
|
||||||
|
|
||||||
|
// Pre-packed data words (registered snapshot of CDC'd data)
|
||||||
|
reg [31:0] data_pkt_word0;
|
||||||
|
reg [31:0] data_pkt_word1;
|
||||||
|
reg [31:0] data_pkt_word2;
|
||||||
|
reg [3:0] data_pkt_be2; // BE for last word (BE=1110 since byte 3 is pad)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// READ FSM State definitions (Gap 4: USB Read Path)
|
// READ FSM State definitions (Gap 4: USB Read Path)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -184,6 +208,67 @@ always @(posedge clk or negedge reset_n) begin
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// CLOCK-ACTIVITY WATCHDOG (clk domain)
|
||||||
|
// ============================================================================
|
||||||
|
// Detects when ft601_clk_in stops (USB cable unplugged). A toggle register
|
||||||
|
// in the ft601_clk domain flips every edge. The clk domain synchronizes it
|
||||||
|
// and checks for transitions. If no transition is seen for 2^16 = 65536
|
||||||
|
// clk cycles (~0.65 ms at 100 MHz), ft601_clk_lost asserts.
|
||||||
|
|
||||||
|
// Toggle register: flips every ft601_clk edge (ft601_clk domain)
|
||||||
|
reg ft601_heartbeat;
|
||||||
|
always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
|
||||||
|
if (!ft601_reset_n)
|
||||||
|
ft601_heartbeat <= 1'b0;
|
||||||
|
else
|
||||||
|
ft601_heartbeat <= ~ft601_heartbeat;
|
||||||
|
end
|
||||||
|
|
||||||
|
// Synchronize heartbeat into clk domain (2-stage)
|
||||||
|
(* ASYNC_REG = "TRUE" *) reg [1:0] ft601_hb_sync;
|
||||||
|
reg ft601_hb_prev;
|
||||||
|
reg [15:0] ft601_clk_timeout;
|
||||||
|
reg ft601_clk_lost;
|
||||||
|
|
||||||
|
always @(posedge clk or negedge reset_n) begin
|
||||||
|
if (!reset_n) begin
|
||||||
|
ft601_hb_sync <= 2'b00;
|
||||||
|
ft601_hb_prev <= 1'b0;
|
||||||
|
ft601_clk_timeout <= 16'd0;
|
||||||
|
ft601_clk_lost <= 1'b0;
|
||||||
|
end else begin
|
||||||
|
ft601_hb_sync <= {ft601_hb_sync[0], ft601_heartbeat};
|
||||||
|
ft601_hb_prev <= ft601_hb_sync[1];
|
||||||
|
|
||||||
|
if (ft601_hb_sync[1] != ft601_hb_prev) begin
|
||||||
|
// ft601_clk is alive — reset counter, clear lost flag
|
||||||
|
ft601_clk_timeout <= 16'd0;
|
||||||
|
ft601_clk_lost <= 1'b0;
|
||||||
|
end else if (!ft601_clk_lost) begin
|
||||||
|
if (ft601_clk_timeout == 16'hFFFF)
|
||||||
|
ft601_clk_lost <= 1'b1;
|
||||||
|
else
|
||||||
|
ft601_clk_timeout <= ft601_clk_timeout + 16'd1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
// Effective FT601-domain reset: asserted by global reset OR clock loss.
|
||||||
|
// Deassertion synchronized to ft601_clk via 2-stage sync to avoid
|
||||||
|
// metastability on the recovery edge.
|
||||||
|
(* ASYNC_REG = "TRUE" *) reg [1:0] ft601_reset_sync;
|
||||||
|
wire ft601_reset_raw_n = ft601_reset_n & ~ft601_clk_lost;
|
||||||
|
|
||||||
|
always @(posedge ft601_clk_in or negedge ft601_reset_raw_n) begin
|
||||||
|
if (!ft601_reset_raw_n)
|
||||||
|
ft601_reset_sync <= 2'b00;
|
||||||
|
else
|
||||||
|
ft601_reset_sync <= {ft601_reset_sync[0], 1'b1};
|
||||||
|
end
|
||||||
|
|
||||||
|
wire ft601_effective_reset_n = ft601_reset_sync[1];
|
||||||
|
|
||||||
// FT601-domain captured data (sampled from holding regs on sync'd edge)
|
// FT601-domain captured data (sampled from holding regs on sync'd edge)
|
||||||
reg [31:0] range_profile_cap;
|
reg [31:0] range_profile_cap;
|
||||||
reg [15:0] doppler_real_cap;
|
reg [15:0] doppler_real_cap;
|
||||||
@@ -197,6 +282,18 @@ reg cfar_detection_cap;
|
|||||||
reg doppler_data_pending;
|
reg doppler_data_pending;
|
||||||
reg cfar_data_pending;
|
reg cfar_data_pending;
|
||||||
|
|
||||||
|
// 1-cycle delayed range trigger. range_valid_ft fires on the same clock
|
||||||
|
// edge that range_profile_cap is captured (non-blocking). If the FSM
|
||||||
|
// reads range_profile_cap on that same edge it sees the STALE value.
|
||||||
|
// Delaying the trigger by one cycle guarantees the capture register has
|
||||||
|
// settled before the FSM packs the data words.
|
||||||
|
reg range_data_ready;
|
||||||
|
|
||||||
|
// Frame sync: sample counter (ft601_clk domain, wraps at NUM_CELLS)
|
||||||
|
// Bit 7 of detection byte is set when sample_counter == 0 (frame start).
|
||||||
|
localparam [11:0] NUM_CELLS = 12'd2048; // 64 range x 32 doppler
|
||||||
|
reg [11:0] sample_counter;
|
||||||
|
|
||||||
// Gap 2: CDC for stream_control (clk_100m -> ft601_clk_in)
|
// Gap 2: CDC for stream_control (clk_100m -> ft601_clk_in)
|
||||||
// stream_control changes infrequently (only on host USB command), so
|
// stream_control changes infrequently (only on host USB command), so
|
||||||
// per-bit 2-stage synchronizers are sufficient. No Gray coding needed
|
// per-bit 2-stage synchronizers are sufficient. No Gray coding needed
|
||||||
@@ -228,8 +325,8 @@ wire range_valid_ft;
|
|||||||
wire doppler_valid_ft;
|
wire doppler_valid_ft;
|
||||||
wire cfar_valid_ft;
|
wire cfar_valid_ft;
|
||||||
|
|
||||||
always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
|
always @(posedge ft601_clk_in or negedge ft601_effective_reset_n) begin
|
||||||
if (!ft601_reset_n) begin
|
if (!ft601_effective_reset_n) begin
|
||||||
range_valid_sync <= 2'b00;
|
range_valid_sync <= 2'b00;
|
||||||
doppler_valid_sync <= 2'b00;
|
doppler_valid_sync <= 2'b00;
|
||||||
cfar_valid_sync <= 2'b00;
|
cfar_valid_sync <= 2'b00;
|
||||||
@@ -240,6 +337,7 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
|
|||||||
doppler_real_cap <= 16'd0;
|
doppler_real_cap <= 16'd0;
|
||||||
doppler_imag_cap <= 16'd0;
|
doppler_imag_cap <= 16'd0;
|
||||||
cfar_detection_cap <= 1'b0;
|
cfar_detection_cap <= 1'b0;
|
||||||
|
range_data_ready <= 1'b0;
|
||||||
// Fix #5: Default to range-only on reset (prevents write FSM deadlock)
|
// Fix #5: Default to range-only on reset (prevents write FSM deadlock)
|
||||||
stream_ctrl_sync_0 <= 3'b001;
|
stream_ctrl_sync_0 <= 3'b001;
|
||||||
stream_ctrl_sync_1 <= 3'b001;
|
stream_ctrl_sync_1 <= 3'b001;
|
||||||
@@ -276,7 +374,7 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
|
|||||||
// Word 4: AGC metrics + range_mode
|
// Word 4: AGC metrics + range_mode
|
||||||
status_words[4] <= {status_agc_current_gain, // [31:28]
|
status_words[4] <= {status_agc_current_gain, // [31:28]
|
||||||
status_agc_peak_magnitude, // [27:20]
|
status_agc_peak_magnitude, // [27:20]
|
||||||
status_agc_saturation_count, // [19:12]
|
status_agc_saturation_count, // [19:12] 8-bit saturation count
|
||||||
status_agc_enable, // [11]
|
status_agc_enable, // [11]
|
||||||
9'd0, // [10:2] reserved
|
9'd0, // [10:2] reserved
|
||||||
status_range_mode}; // [1:0]
|
status_range_mode}; // [1:0]
|
||||||
@@ -302,6 +400,10 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
|
|||||||
if (cfar_valid_sync[1] && !cfar_valid_sync_d) begin
|
if (cfar_valid_sync[1] && !cfar_valid_sync_d) begin
|
||||||
cfar_detection_cap <= cfar_detection_hold;
|
cfar_detection_cap <= cfar_detection_hold;
|
||||||
end
|
end
|
||||||
|
|
||||||
|
// 1-cycle delayed trigger: ensures range_profile_cap has settled
|
||||||
|
// before the FSM reads it for word packing.
|
||||||
|
range_data_ready <= range_valid_ft;
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -314,11 +416,11 @@ assign cfar_valid_ft = cfar_valid_sync[1] && !cfar_valid_sync_d;
|
|||||||
// FT601 data bus direction control
|
// FT601 data bus direction control
|
||||||
assign ft601_data = ft601_data_oe ? ft601_data_out : 32'hzzzz_zzzz;
|
assign ft601_data = ft601_data_oe ? ft601_data_out : 32'hzzzz_zzzz;
|
||||||
|
|
||||||
always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
|
always @(posedge ft601_clk_in or negedge ft601_effective_reset_n) begin
|
||||||
if (!ft601_reset_n) begin
|
if (!ft601_effective_reset_n) begin
|
||||||
current_state <= IDLE;
|
current_state <= IDLE;
|
||||||
read_state <= RD_IDLE;
|
read_state <= RD_IDLE;
|
||||||
byte_counter <= 0;
|
data_word_idx <= 2'd0;
|
||||||
ft601_data_out <= 0;
|
ft601_data_out <= 0;
|
||||||
ft601_data_oe <= 0;
|
ft601_data_oe <= 0;
|
||||||
ft601_be <= 4'b1111; // All bytes enabled for 32-bit mode
|
ft601_be <= 4'b1111; // All bytes enabled for 32-bit mode
|
||||||
@@ -336,6 +438,11 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
|
|||||||
cmd_value <= 16'd0;
|
cmd_value <= 16'd0;
|
||||||
doppler_data_pending <= 1'b0;
|
doppler_data_pending <= 1'b0;
|
||||||
cfar_data_pending <= 1'b0;
|
cfar_data_pending <= 1'b0;
|
||||||
|
data_pkt_word0 <= 32'd0;
|
||||||
|
data_pkt_word1 <= 32'd0;
|
||||||
|
data_pkt_word2 <= 32'd0;
|
||||||
|
data_pkt_be2 <= 4'b1110;
|
||||||
|
sample_counter <= 12'd0;
|
||||||
// NOTE: ft601_clk_out is driven by the clk-domain always block below.
|
// NOTE: ft601_clk_out is driven by the clk-domain always block below.
|
||||||
// Do NOT assign it here (ft601_clk_in domain) — causes multi-driven net.
|
// Do NOT assign it here (ft601_clk_in domain) — causes multi-driven net.
|
||||||
end else begin
|
end else begin
|
||||||
@@ -424,125 +531,67 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
|
|||||||
current_state <= SEND_STATUS;
|
current_state <= SEND_STATUS;
|
||||||
status_word_idx <= 3'd0;
|
status_word_idx <= 3'd0;
|
||||||
end
|
end
|
||||||
// Trigger write FSM on range_valid edge (primary data source).
|
// Trigger on range_data_ready (1 cycle after range_valid_ft)
|
||||||
// Doppler/cfar data_pending flags are checked inside
|
// so that range_profile_cap has settled from the CDC block.
|
||||||
// SEND_DOPPLER_DATA and SEND_DETECTION_DATA to skip or send.
|
// Gate on pending flags: only send when all enabled
|
||||||
// Do NOT trigger on pending flags alone — they're sticky and
|
// streams have fresh data (avoids stale doppler/CFAR)
|
||||||
// would cause repeated packet starts without new range data.
|
else if (range_data_ready && stream_range_en
|
||||||
else if (range_valid_ft && stream_range_en) begin
|
&& (!stream_doppler_en || doppler_data_pending)
|
||||||
|
&& (!stream_cfar_en || cfar_data_pending)) begin
|
||||||
// Don't start write if a read is about to begin
|
// Don't start write if a read is about to begin
|
||||||
if (ft601_rxf) begin // rxf=1 means no host data pending
|
if (ft601_rxf) begin // rxf=1 means no host data pending
|
||||||
current_state <= SEND_HEADER;
|
// Pack 11-byte data packet into 3 x 32-bit words
|
||||||
byte_counter <= 0;
|
// Doppler fields zeroed when stream disabled
|
||||||
|
// CFAR field zeroed when stream disabled
|
||||||
|
data_pkt_word0 <= {HEADER,
|
||||||
|
range_profile_cap[31:24],
|
||||||
|
range_profile_cap[23:16],
|
||||||
|
range_profile_cap[15:8]};
|
||||||
|
data_pkt_word1 <= {range_profile_cap[7:0],
|
||||||
|
stream_doppler_en ? doppler_real_cap[15:8] : 8'd0,
|
||||||
|
stream_doppler_en ? doppler_real_cap[7:0] : 8'd0,
|
||||||
|
stream_doppler_en ? doppler_imag_cap[15:8] : 8'd0};
|
||||||
|
data_pkt_word2 <= {stream_doppler_en ? doppler_imag_cap[7:0] : 8'd0,
|
||||||
|
stream_cfar_en
|
||||||
|
? {(sample_counter == 12'd0), 6'b0, cfar_detection_cap}
|
||||||
|
: {(sample_counter == 12'd0), 7'd0},
|
||||||
|
FOOTER,
|
||||||
|
8'h00}; // pad byte
|
||||||
|
data_pkt_be2 <= 4'b1110; // 3 valid bytes + 1 pad
|
||||||
|
data_word_idx <= 2'd0;
|
||||||
|
current_state <= SEND_DATA_WORD;
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
SEND_HEADER: begin
|
SEND_DATA_WORD: begin
|
||||||
if (!ft601_txe) begin // FT601 TX FIFO not empty
|
|
||||||
ft601_data_oe <= 1;
|
|
||||||
ft601_data_out <= {24'b0, HEADER};
|
|
||||||
ft601_be <= 4'b0001; // Only lower byte valid
|
|
||||||
ft601_wr_n <= 0; // Assert write strobe
|
|
||||||
// Gap 2: skip to first enabled stream
|
|
||||||
if (stream_range_en)
|
|
||||||
current_state <= SEND_RANGE_DATA;
|
|
||||||
else if (stream_doppler_en)
|
|
||||||
current_state <= SEND_DOPPLER_DATA;
|
|
||||||
else if (stream_cfar_en)
|
|
||||||
current_state <= SEND_DETECTION_DATA;
|
|
||||||
else
|
|
||||||
current_state <= SEND_FOOTER; // No streams — send footer only
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
SEND_RANGE_DATA: begin
|
|
||||||
if (!ft601_txe) begin
|
if (!ft601_txe) begin
|
||||||
ft601_data_oe <= 1;
|
ft601_data_oe <= 1;
|
||||||
ft601_be <= 4'b1111; // All bytes valid for 32-bit word
|
ft601_wr_n <= 0;
|
||||||
|
case (data_word_idx)
|
||||||
case (byte_counter)
|
2'd0: begin
|
||||||
0: ft601_data_out <= range_profile_cap;
|
ft601_data_out <= data_pkt_word0;
|
||||||
1: ft601_data_out <= {range_profile_cap[23:0], 8'h00};
|
ft601_be <= 4'b1111;
|
||||||
2: ft601_data_out <= {range_profile_cap[15:0], 16'h0000};
|
end
|
||||||
3: ft601_data_out <= {range_profile_cap[7:0], 24'h000000};
|
2'd1: begin
|
||||||
|
ft601_data_out <= data_pkt_word1;
|
||||||
|
ft601_be <= 4'b1111;
|
||||||
|
end
|
||||||
|
2'd2: begin
|
||||||
|
ft601_data_out <= data_pkt_word2;
|
||||||
|
ft601_be <= data_pkt_be2;
|
||||||
|
end
|
||||||
|
default: ;
|
||||||
endcase
|
endcase
|
||||||
|
if (data_word_idx == 2'd2) begin
|
||||||
ft601_wr_n <= 0;
|
data_word_idx <= 2'd0;
|
||||||
|
current_state <= WAIT_ACK;
|
||||||
if (byte_counter == 3) begin
|
|
||||||
byte_counter <= 0;
|
|
||||||
// Gap 2: skip disabled streams
|
|
||||||
if (stream_doppler_en)
|
|
||||||
current_state <= SEND_DOPPLER_DATA;
|
|
||||||
else if (stream_cfar_en)
|
|
||||||
current_state <= SEND_DETECTION_DATA;
|
|
||||||
else
|
|
||||||
current_state <= SEND_FOOTER;
|
|
||||||
end else begin
|
end else begin
|
||||||
byte_counter <= byte_counter + 1;
|
data_word_idx <= data_word_idx + 2'd1;
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
SEND_DOPPLER_DATA: begin
|
|
||||||
if (!ft601_txe && doppler_data_pending) begin
|
|
||||||
ft601_data_oe <= 1;
|
|
||||||
ft601_be <= 4'b1111;
|
|
||||||
|
|
||||||
case (byte_counter)
|
|
||||||
0: ft601_data_out <= {doppler_real_cap, doppler_imag_cap};
|
|
||||||
1: ft601_data_out <= {doppler_imag_cap, doppler_real_cap[15:8], 8'h00};
|
|
||||||
2: ft601_data_out <= {doppler_real_cap[7:0], doppler_imag_cap[15:8], 16'h0000};
|
|
||||||
3: ft601_data_out <= {doppler_imag_cap[7:0], 24'h000000};
|
|
||||||
endcase
|
|
||||||
|
|
||||||
ft601_wr_n <= 0;
|
|
||||||
|
|
||||||
if (byte_counter == 3) begin
|
|
||||||
byte_counter <= 0;
|
|
||||||
doppler_data_pending <= 1'b0;
|
|
||||||
if (stream_cfar_en)
|
|
||||||
current_state <= SEND_DETECTION_DATA;
|
|
||||||
else
|
|
||||||
current_state <= SEND_FOOTER;
|
|
||||||
end else begin
|
|
||||||
byte_counter <= byte_counter + 1;
|
|
||||||
end
|
|
||||||
end else if (!doppler_data_pending) begin
|
|
||||||
// No doppler data available yet — skip to next stream
|
|
||||||
byte_counter <= 0;
|
|
||||||
if (stream_cfar_en)
|
|
||||||
current_state <= SEND_DETECTION_DATA;
|
|
||||||
else
|
|
||||||
current_state <= SEND_FOOTER;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
SEND_DETECTION_DATA: begin
|
|
||||||
if (!ft601_txe && cfar_data_pending) begin
|
|
||||||
ft601_data_oe <= 1;
|
|
||||||
ft601_be <= 4'b0001;
|
|
||||||
ft601_data_out <= {24'b0, 7'b0, cfar_detection_cap};
|
|
||||||
ft601_wr_n <= 0;
|
|
||||||
cfar_data_pending <= 1'b0;
|
|
||||||
current_state <= SEND_FOOTER;
|
|
||||||
end else if (!cfar_data_pending) begin
|
|
||||||
// No CFAR data available yet — skip to footer
|
|
||||||
current_state <= SEND_FOOTER;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
SEND_FOOTER: begin
|
|
||||||
if (!ft601_txe) begin
|
|
||||||
ft601_data_oe <= 1;
|
|
||||||
ft601_be <= 4'b0001;
|
|
||||||
ft601_data_out <= {24'b0, FOOTER};
|
|
||||||
ft601_wr_n <= 0;
|
|
||||||
current_state <= WAIT_ACK;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
// Gap 2: Status readback — send 6 x 32-bit status words
|
// Gap 2: Status readback — send 6 x 32-bit status words
|
||||||
// Format: HEADER, status_words[0..5], FOOTER
|
// Format: HEADER, status_words[0..5], FOOTER
|
||||||
SEND_STATUS: begin
|
SEND_STATUS: begin
|
||||||
@@ -581,6 +630,14 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
|
|||||||
WAIT_ACK: begin
|
WAIT_ACK: begin
|
||||||
ft601_wr_n <= 1;
|
ft601_wr_n <= 1;
|
||||||
ft601_data_oe <= 0; // Release data bus
|
ft601_data_oe <= 0; // Release data bus
|
||||||
|
// Clear pending flags — data consumed
|
||||||
|
doppler_data_pending <= 1'b0;
|
||||||
|
cfar_data_pending <= 1'b0;
|
||||||
|
// Advance frame sync counter
|
||||||
|
if (sample_counter == NUM_CELLS - 12'd1)
|
||||||
|
sample_counter <= 12'd0;
|
||||||
|
else
|
||||||
|
sample_counter <= sample_counter + 12'd1;
|
||||||
current_state <= IDLE;
|
current_state <= IDLE;
|
||||||
end
|
end
|
||||||
endcase
|
endcase
|
||||||
@@ -613,8 +670,8 @@ ODDR #(
|
|||||||
`else
|
`else
|
||||||
// Simulation: behavioral clock forwarding
|
// Simulation: behavioral clock forwarding
|
||||||
reg ft601_clk_out_sim;
|
reg ft601_clk_out_sim;
|
||||||
always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
|
always @(posedge ft601_clk_in or negedge ft601_effective_reset_n) begin
|
||||||
if (!ft601_reset_n)
|
if (!ft601_effective_reset_n)
|
||||||
ft601_clk_out_sim <= 1'b0;
|
ft601_clk_out_sim <= 1'b0;
|
||||||
else
|
else
|
||||||
ft601_clk_out_sim <= 1'b1;
|
ft601_clk_out_sim <= 1'b1;
|
||||||
|
|||||||
@@ -36,6 +36,13 @@
|
|||||||
* Clock domains:
|
* Clock domains:
|
||||||
* clk = 100 MHz system clock (radar data domain)
|
* clk = 100 MHz system clock (radar data domain)
|
||||||
* ft_clk = 60 MHz from FT2232H CLKOUT (USB FIFO domain)
|
* ft_clk = 60 MHz from FT2232H CLKOUT (USB FIFO domain)
|
||||||
|
*
|
||||||
|
* USB disconnect recovery:
|
||||||
|
* A clock-activity watchdog in the clk domain detects when ft_clk stops
|
||||||
|
* (USB cable unplugged). After ~0.65 ms of silence (65536 system clocks)
|
||||||
|
* it asserts ft_clk_lost, which is OR'd into the FT-domain reset so
|
||||||
|
* FSMs and FIFOs return to a clean state. When ft_clk resumes, a 2-stage
|
||||||
|
* reset synchronizer deasserts the reset cleanly in the ft_clk domain.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module usb_data_interface_ft2232h (
|
module usb_data_interface_ft2232h (
|
||||||
@@ -59,7 +66,9 @@ module usb_data_interface_ft2232h (
|
|||||||
output reg ft_rd_n, // Read strobe (active low)
|
output reg ft_rd_n, // Read strobe (active low)
|
||||||
output reg ft_wr_n, // Write strobe (active low)
|
output reg ft_wr_n, // Write strobe (active low)
|
||||||
output reg ft_oe_n, // Output enable (active low) — bus direction
|
output reg ft_oe_n, // Output enable (active low) — bus direction
|
||||||
output reg ft_siwu, // Send Immediate / WakeUp
|
output reg ft_siwu, // Send Immediate / WakeUp — UNUSED: held low.
|
||||||
|
// SIWU could flush the TX FIFO for lower latency
|
||||||
|
// but is not needed at current data rates. Deferred.
|
||||||
|
|
||||||
// Clock from FT2232H (directly used — no ODDR forwarding needed)
|
// Clock from FT2232H (directly used — no ODDR forwarding needed)
|
||||||
input wire ft_clk, // 60 MHz from FT2232H CLKOUT
|
input wire ft_clk, // 60 MHz from FT2232H CLKOUT
|
||||||
@@ -134,6 +143,7 @@ localparam [2:0] RD_IDLE = 3'd0,
|
|||||||
reg [2:0] rd_state;
|
reg [2:0] rd_state;
|
||||||
reg [1:0] rd_byte_cnt; // 0..3 for 4-byte command word
|
reg [1:0] rd_byte_cnt; // 0..3 for 4-byte command word
|
||||||
reg [31:0] rd_shift_reg; // Shift register to assemble 4-byte command
|
reg [31:0] rd_shift_reg; // Shift register to assemble 4-byte command
|
||||||
|
reg rd_cmd_complete; // Set when all 4 bytes received (distinguishes from abort)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// DATA BUS DIRECTION CONTROL
|
// DATA BUS DIRECTION CONTROL
|
||||||
@@ -192,6 +202,70 @@ always @(posedge clk or negedge reset_n) begin
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// CLOCK-ACTIVITY WATCHDOG (clk domain)
|
||||||
|
// ============================================================================
|
||||||
|
// Detects when ft_clk stops (USB cable unplugged). A toggle register in the
|
||||||
|
// ft_clk domain flips every ft_clk edge. The clk domain synchronizes it and
|
||||||
|
// checks for transitions. If no transition is seen for 2^16 = 65536 clk
|
||||||
|
// cycles (~0.65 ms at 100 MHz), ft_clk_lost asserts.
|
||||||
|
//
|
||||||
|
// ft_clk_lost feeds into the effective reset for the ft_clk domain so that
|
||||||
|
// FSMs and capture registers return to a clean state automatically.
|
||||||
|
|
||||||
|
// Toggle register: flips every ft_clk edge (ft_clk domain)
|
||||||
|
reg ft_heartbeat;
|
||||||
|
always @(posedge ft_clk or negedge ft_reset_n) begin
|
||||||
|
if (!ft_reset_n)
|
||||||
|
ft_heartbeat <= 1'b0;
|
||||||
|
else
|
||||||
|
ft_heartbeat <= ~ft_heartbeat;
|
||||||
|
end
|
||||||
|
|
||||||
|
// Synchronize heartbeat into clk domain (2-stage)
|
||||||
|
(* ASYNC_REG = "TRUE" *) reg [1:0] ft_hb_sync;
|
||||||
|
reg ft_hb_prev;
|
||||||
|
reg [15:0] ft_clk_timeout;
|
||||||
|
reg ft_clk_lost;
|
||||||
|
|
||||||
|
always @(posedge clk or negedge reset_n) begin
|
||||||
|
if (!reset_n) begin
|
||||||
|
ft_hb_sync <= 2'b00;
|
||||||
|
ft_hb_prev <= 1'b0;
|
||||||
|
ft_clk_timeout <= 16'd0;
|
||||||
|
ft_clk_lost <= 1'b0;
|
||||||
|
end else begin
|
||||||
|
ft_hb_sync <= {ft_hb_sync[0], ft_heartbeat};
|
||||||
|
ft_hb_prev <= ft_hb_sync[1];
|
||||||
|
|
||||||
|
if (ft_hb_sync[1] != ft_hb_prev) begin
|
||||||
|
// ft_clk is alive — reset counter, clear lost flag
|
||||||
|
ft_clk_timeout <= 16'd0;
|
||||||
|
ft_clk_lost <= 1'b0;
|
||||||
|
end else if (!ft_clk_lost) begin
|
||||||
|
if (ft_clk_timeout == 16'hFFFF)
|
||||||
|
ft_clk_lost <= 1'b1;
|
||||||
|
else
|
||||||
|
ft_clk_timeout <= ft_clk_timeout + 16'd1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
// Effective FT-domain reset: asserted by global reset OR clock loss.
|
||||||
|
// Deassertion synchronized to ft_clk via 2-stage sync to avoid
|
||||||
|
// metastability on the recovery edge.
|
||||||
|
(* ASYNC_REG = "TRUE" *) reg [1:0] ft_reset_sync;
|
||||||
|
wire ft_reset_raw_n = ft_reset_n & ~ft_clk_lost;
|
||||||
|
|
||||||
|
always @(posedge ft_clk or negedge ft_reset_raw_n) begin
|
||||||
|
if (!ft_reset_raw_n)
|
||||||
|
ft_reset_sync <= 2'b00;
|
||||||
|
else
|
||||||
|
ft_reset_sync <= {ft_reset_sync[0], 1'b1};
|
||||||
|
end
|
||||||
|
|
||||||
|
wire ft_effective_reset_n = ft_reset_sync[1];
|
||||||
|
|
||||||
// --- 3-stage synchronizers (ft_clk domain) ---
|
// --- 3-stage synchronizers (ft_clk domain) ---
|
||||||
// 3 stages for better MTBF at 60 MHz
|
// 3 stages for better MTBF at 60 MHz
|
||||||
|
|
||||||
@@ -228,12 +302,25 @@ reg cfar_detection_cap;
|
|||||||
reg doppler_data_pending;
|
reg doppler_data_pending;
|
||||||
reg cfar_data_pending;
|
reg cfar_data_pending;
|
||||||
|
|
||||||
|
// 1-cycle delayed range trigger. range_valid_ft fires on the same clock
|
||||||
|
// edge that range_profile_cap is captured (non-blocking). If the FSM
|
||||||
|
// reads range_profile_cap on that same edge it sees the STALE value.
|
||||||
|
// Delaying the trigger by one cycle guarantees the capture register has
|
||||||
|
// settled before the byte mux reads it.
|
||||||
|
reg range_data_ready;
|
||||||
|
|
||||||
|
// Frame sync: sample counter (ft_clk domain, wraps at NUM_CELLS)
|
||||||
|
// Bit 7 of detection byte is set when sample_counter == 0 (frame start).
|
||||||
|
// This allows the Python host to resynchronize without a protocol change.
|
||||||
|
localparam [11:0] NUM_CELLS = 12'd2048; // 64 range x 32 doppler
|
||||||
|
reg [11:0] sample_counter;
|
||||||
|
|
||||||
// Status snapshot (ft_clk domain)
|
// Status snapshot (ft_clk domain)
|
||||||
reg [31:0] status_words [0:5];
|
reg [31:0] status_words [0:5];
|
||||||
|
|
||||||
integer si; // status_words loop index
|
integer si; // status_words loop index
|
||||||
always @(posedge ft_clk or negedge ft_reset_n) begin
|
always @(posedge ft_clk or negedge ft_effective_reset_n) begin
|
||||||
if (!ft_reset_n) begin
|
if (!ft_effective_reset_n) begin
|
||||||
range_toggle_sync <= 3'b000;
|
range_toggle_sync <= 3'b000;
|
||||||
doppler_toggle_sync <= 3'b000;
|
doppler_toggle_sync <= 3'b000;
|
||||||
cfar_toggle_sync <= 3'b000;
|
cfar_toggle_sync <= 3'b000;
|
||||||
@@ -246,6 +333,7 @@ always @(posedge ft_clk or negedge ft_reset_n) begin
|
|||||||
doppler_real_cap <= 16'd0;
|
doppler_real_cap <= 16'd0;
|
||||||
doppler_imag_cap <= 16'd0;
|
doppler_imag_cap <= 16'd0;
|
||||||
cfar_detection_cap <= 1'b0;
|
cfar_detection_cap <= 1'b0;
|
||||||
|
range_data_ready <= 1'b0;
|
||||||
// Default to range-only on reset (prevents write FSM deadlock)
|
// Default to range-only on reset (prevents write FSM deadlock)
|
||||||
stream_ctrl_sync_0 <= 3'b001;
|
stream_ctrl_sync_0 <= 3'b001;
|
||||||
stream_ctrl_sync_1 <= 3'b001;
|
stream_ctrl_sync_1 <= 3'b001;
|
||||||
@@ -279,6 +367,10 @@ always @(posedge ft_clk or negedge ft_reset_n) begin
|
|||||||
if (cfar_valid_ft)
|
if (cfar_valid_ft)
|
||||||
cfar_detection_cap <= cfar_detection_hold;
|
cfar_detection_cap <= cfar_detection_hold;
|
||||||
|
|
||||||
|
// 1-cycle delayed trigger: ensures range_profile_cap has settled
|
||||||
|
// before the FSM reads it via the byte mux.
|
||||||
|
range_data_ready <= range_valid_ft;
|
||||||
|
|
||||||
// Status snapshot on request
|
// Status snapshot on request
|
||||||
if (status_req_ft) begin
|
if (status_req_ft) begin
|
||||||
// Word 0: {0xFF[31:24], mode[23:22], stream[21:19], 3'b000[18:16], threshold[15:0]}
|
// Word 0: {0xFF[31:24], mode[23:22], stream[21:19], 3'b000[18:16], threshold[15:0]}
|
||||||
@@ -315,11 +407,16 @@ always @(*) begin
|
|||||||
5'd2: data_pkt_byte = range_profile_cap[23:16];
|
5'd2: data_pkt_byte = range_profile_cap[23:16];
|
||||||
5'd3: data_pkt_byte = range_profile_cap[15:8];
|
5'd3: data_pkt_byte = range_profile_cap[15:8];
|
||||||
5'd4: data_pkt_byte = range_profile_cap[7:0]; // range LSB
|
5'd4: data_pkt_byte = range_profile_cap[7:0]; // range LSB
|
||||||
5'd5: data_pkt_byte = doppler_real_cap[15:8]; // doppler_real MSB
|
// Doppler fields: zero when stream_doppler_en is off
|
||||||
5'd6: data_pkt_byte = doppler_real_cap[7:0]; // doppler_real LSB
|
5'd5: data_pkt_byte = stream_doppler_en ? doppler_real_cap[15:8] : 8'd0;
|
||||||
5'd7: data_pkt_byte = doppler_imag_cap[15:8]; // doppler_imag MSB
|
5'd6: data_pkt_byte = stream_doppler_en ? doppler_real_cap[7:0] : 8'd0;
|
||||||
5'd8: data_pkt_byte = doppler_imag_cap[7:0]; // doppler_imag LSB
|
5'd7: data_pkt_byte = stream_doppler_en ? doppler_imag_cap[15:8] : 8'd0;
|
||||||
5'd9: data_pkt_byte = {7'b0, cfar_detection_cap}; // detection
|
5'd8: data_pkt_byte = stream_doppler_en ? doppler_imag_cap[7:0] : 8'd0;
|
||||||
|
// Detection field: zero when stream_cfar_en is off
|
||||||
|
// Bit 7 = frame_start flag (sample_counter == 0), bit 0 = cfar_detection
|
||||||
|
5'd9: data_pkt_byte = stream_cfar_en
|
||||||
|
? {(sample_counter == 12'd0), 6'b0, cfar_detection_cap}
|
||||||
|
: {(sample_counter == 12'd0), 7'd0};
|
||||||
5'd10: data_pkt_byte = FOOTER;
|
5'd10: data_pkt_byte = FOOTER;
|
||||||
default: data_pkt_byte = 8'h00;
|
default: data_pkt_byte = 8'h00;
|
||||||
endcase
|
endcase
|
||||||
@@ -376,12 +473,13 @@ end
|
|||||||
// Write FSM and Read FSM share the bus. Write FSM operates when Read FSM
|
// Write FSM and Read FSM share the bus. Write FSM operates when Read FSM
|
||||||
// is idle. Read FSM takes priority when host has data available.
|
// is idle. Read FSM takes priority when host has data available.
|
||||||
|
|
||||||
always @(posedge ft_clk or negedge ft_reset_n) begin
|
always @(posedge ft_clk or negedge ft_effective_reset_n) begin
|
||||||
if (!ft_reset_n) begin
|
if (!ft_effective_reset_n) begin
|
||||||
wr_state <= WR_IDLE;
|
wr_state <= WR_IDLE;
|
||||||
wr_byte_idx <= 5'd0;
|
wr_byte_idx <= 5'd0;
|
||||||
rd_state <= RD_IDLE;
|
rd_state <= RD_IDLE;
|
||||||
rd_byte_cnt <= 2'd0;
|
rd_byte_cnt <= 2'd0;
|
||||||
|
rd_cmd_complete <= 1'b0;
|
||||||
rd_shift_reg <= 32'd0;
|
rd_shift_reg <= 32'd0;
|
||||||
ft_data_out <= 8'd0;
|
ft_data_out <= 8'd0;
|
||||||
ft_data_oe <= 1'b0;
|
ft_data_oe <= 1'b0;
|
||||||
@@ -396,6 +494,7 @@ always @(posedge ft_clk or negedge ft_reset_n) begin
|
|||||||
cmd_value <= 16'd0;
|
cmd_value <= 16'd0;
|
||||||
doppler_data_pending <= 1'b0;
|
doppler_data_pending <= 1'b0;
|
||||||
cfar_data_pending <= 1'b0;
|
cfar_data_pending <= 1'b0;
|
||||||
|
sample_counter <= 12'd0;
|
||||||
end else begin
|
end else begin
|
||||||
// Default: clear one-shot signals
|
// Default: clear one-shot signals
|
||||||
cmd_valid <= 1'b0;
|
cmd_valid <= 1'b0;
|
||||||
@@ -437,17 +536,19 @@ always @(posedge ft_clk or negedge ft_reset_n) begin
|
|||||||
rd_shift_reg <= {rd_shift_reg[23:0], ft_data};
|
rd_shift_reg <= {rd_shift_reg[23:0], ft_data};
|
||||||
if (rd_byte_cnt == 2'd3) begin
|
if (rd_byte_cnt == 2'd3) begin
|
||||||
// All 4 bytes received
|
// All 4 bytes received
|
||||||
ft_rd_n <= 1'b1;
|
ft_rd_n <= 1'b1;
|
||||||
rd_byte_cnt <= 2'd0;
|
rd_byte_cnt <= 2'd0;
|
||||||
rd_state <= RD_DEASSERT;
|
rd_cmd_complete <= 1'b1;
|
||||||
|
rd_state <= RD_DEASSERT;
|
||||||
end else begin
|
end else begin
|
||||||
rd_byte_cnt <= rd_byte_cnt + 2'd1;
|
rd_byte_cnt <= rd_byte_cnt + 2'd1;
|
||||||
// Keep reading if more data available
|
// Keep reading if more data available
|
||||||
if (ft_rxf_n) begin
|
if (ft_rxf_n) begin
|
||||||
// Host ran out of data mid-command — abort
|
// Host ran out of data mid-command — abort
|
||||||
ft_rd_n <= 1'b1;
|
ft_rd_n <= 1'b1;
|
||||||
rd_byte_cnt <= 2'd0;
|
rd_byte_cnt <= 2'd0;
|
||||||
rd_state <= RD_DEASSERT;
|
rd_cmd_complete <= 1'b0;
|
||||||
|
rd_state <= RD_DEASSERT;
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -456,7 +557,8 @@ always @(posedge ft_clk or negedge ft_reset_n) begin
|
|||||||
// Deassert OE (1 cycle after RD deasserted)
|
// Deassert OE (1 cycle after RD deasserted)
|
||||||
ft_oe_n <= 1'b1;
|
ft_oe_n <= 1'b1;
|
||||||
// Only process if we received a full 4-byte command
|
// Only process if we received a full 4-byte command
|
||||||
if (rd_byte_cnt == 2'd0) begin
|
if (rd_cmd_complete) begin
|
||||||
|
rd_cmd_complete <= 1'b0;
|
||||||
rd_state <= RD_PROCESS;
|
rd_state <= RD_PROCESS;
|
||||||
end else begin
|
end else begin
|
||||||
// Incomplete command — discard
|
// Incomplete command — discard
|
||||||
@@ -491,8 +593,13 @@ always @(posedge ft_clk or negedge ft_reset_n) begin
|
|||||||
wr_state <= WR_STATUS_SEND;
|
wr_state <= WR_STATUS_SEND;
|
||||||
wr_byte_idx <= 5'd0;
|
wr_byte_idx <= 5'd0;
|
||||||
end
|
end
|
||||||
// Trigger on range_valid edge (primary data trigger)
|
// Trigger on range_data_ready (1 cycle after range_valid_ft)
|
||||||
else if (range_valid_ft && stream_range_en) begin
|
// so that range_profile_cap has settled from the CDC block.
|
||||||
|
// Gate on pending flags: only send when all enabled
|
||||||
|
// streams have fresh data (avoids stale doppler/CFAR)
|
||||||
|
else if (range_data_ready && stream_range_en
|
||||||
|
&& (!stream_doppler_en || doppler_data_pending)
|
||||||
|
&& (!stream_cfar_en || cfar_data_pending)) begin
|
||||||
if (ft_rxf_n) begin // No host read pending
|
if (ft_rxf_n) begin // No host read pending
|
||||||
wr_state <= WR_DATA_SEND;
|
wr_state <= WR_DATA_SEND;
|
||||||
wr_byte_idx <= 5'd0;
|
wr_byte_idx <= 5'd0;
|
||||||
@@ -538,6 +645,11 @@ always @(posedge ft_clk or negedge ft_reset_n) begin
|
|||||||
// Clear pending flags — data consumed
|
// Clear pending flags — data consumed
|
||||||
doppler_data_pending <= 1'b0;
|
doppler_data_pending <= 1'b0;
|
||||||
cfar_data_pending <= 1'b0;
|
cfar_data_pending <= 1'b0;
|
||||||
|
// Advance frame sync counter
|
||||||
|
if (sample_counter == NUM_CELLS - 12'd1)
|
||||||
|
sample_counter <= 12'd0;
|
||||||
|
else
|
||||||
|
sample_counter <= sample_counter + 12'd1;
|
||||||
wr_state <= WR_IDLE;
|
wr_state <= WR_IDLE;
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# DEPRECATED: GUI V6 is superseded by GUI_V65_Tk (tkinter) and V7 (PyQt6).
|
||||||
|
# This file is retained for reference only. Do not use for new development.
|
||||||
|
# Removal planned for next major release.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, messagebox
|
from tkinter import ttk, messagebox
|
||||||
import threading
|
import threading
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ except (ModuleNotFoundError, ImportError):
|
|||||||
|
|
||||||
# Import protocol layer (no GUI deps)
|
# Import protocol layer (no GUI deps)
|
||||||
from radar_protocol import (
|
from radar_protocol import (
|
||||||
RadarProtocol, FT2232HConnection,
|
RadarProtocol, FT2232HConnection, FT601Connection,
|
||||||
DataRecorder, RadarAcquisition,
|
DataRecorder, RadarAcquisition,
|
||||||
RadarFrame, StatusResponse,
|
RadarFrame, StatusResponse,
|
||||||
NUM_RANGE_BINS, NUM_DOPPLER_BINS, WATERFALL_DEPTH,
|
NUM_RANGE_BINS, NUM_DOPPLER_BINS, WATERFALL_DEPTH,
|
||||||
@@ -98,9 +98,10 @@ class DemoTarget:
|
|||||||
|
|
||||||
__slots__ = ("azimuth", "classification", "id", "range_m", "snr", "velocity")
|
__slots__ = ("azimuth", "classification", "id", "range_m", "snr", "velocity")
|
||||||
|
|
||||||
# Physical range grid: 64 bins x ~4.8 m/bin = ~307 m max
|
# Physical range grid: 64 bins x ~24 m/bin = ~1536 m max
|
||||||
_RANGE_PER_BIN: float = (3e8 / (2 * 500e6)) * 16 # ~4.8 m
|
# Bin spacing = c / (2 * Fs) * decimation, where Fs = 100 MHz DDC output.
|
||||||
_MAX_RANGE: float = _RANGE_PER_BIN * NUM_RANGE_BINS # ~307 m
|
_RANGE_PER_BIN: float = (3e8 / (2 * 100e6)) * 16 # ~24 m
|
||||||
|
_MAX_RANGE: float = _RANGE_PER_BIN * NUM_RANGE_BINS # ~1536 m
|
||||||
|
|
||||||
def __init__(self, tid: int):
|
def __init__(self, tid: int):
|
||||||
self.id = tid
|
self.id = tid
|
||||||
@@ -187,10 +188,10 @@ class DemoSimulator:
|
|||||||
mag = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.float64)
|
mag = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.float64)
|
||||||
det = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.uint8)
|
det = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.uint8)
|
||||||
|
|
||||||
# Range/Doppler scaling (approximate)
|
# Range/Doppler scaling: bin spacing = c/(2*Fs)*decimation
|
||||||
range_per_bin = (3e8 / (2 * 500e6)) * 16 # ~4.8 m/bin
|
range_per_bin = (3e8 / (2 * 100e6)) * 16 # ~24 m/bin
|
||||||
max_range = range_per_bin * NUM_RANGE_BINS
|
max_range = range_per_bin * NUM_RANGE_BINS
|
||||||
vel_per_bin = 1.484 # m/s per Doppler bin (from WaveformConfig)
|
vel_per_bin = 5.34 # m/s per Doppler bin (radar_scene.py: lam/(2*16*PRI))
|
||||||
|
|
||||||
for t in targets:
|
for t in targets:
|
||||||
if t.range_m > max_range or t.range_m < 0:
|
if t.range_m > max_range or t.range_m < 0:
|
||||||
@@ -385,13 +386,14 @@ class RadarDashboard:
|
|||||||
UPDATE_INTERVAL_MS = 100 # 10 Hz display refresh
|
UPDATE_INTERVAL_MS = 100 # 10 Hz display refresh
|
||||||
|
|
||||||
# Radar parameters used for range-axis scaling.
|
# Radar parameters used for range-axis scaling.
|
||||||
BANDWIDTH = 500e6 # Hz — chirp bandwidth
|
SAMPLE_RATE = 100e6 # Hz — DDC output I/Q rate (matched filter input)
|
||||||
C = 3e8 # m/s — speed of light
|
C = 3e8 # m/s — speed of light
|
||||||
|
|
||||||
def __init__(self, root: tk.Tk, connection: FT2232HConnection,
|
def __init__(self, root: tk.Tk, mock: bool,
|
||||||
recorder: DataRecorder, device_index: int = 0):
|
recorder: DataRecorder, device_index: int = 0):
|
||||||
self.root = root
|
self.root = root
|
||||||
self.conn = connection
|
self._mock = mock
|
||||||
|
self.conn: FT2232HConnection | FT601Connection | None = None
|
||||||
self.recorder = recorder
|
self.recorder = recorder
|
||||||
self.device_index = device_index
|
self.device_index = device_index
|
||||||
|
|
||||||
@@ -485,6 +487,16 @@ class RadarDashboard:
|
|||||||
style="Accent.TButton")
|
style="Accent.TButton")
|
||||||
self.btn_connect.pack(side="right", padx=4)
|
self.btn_connect.pack(side="right", padx=4)
|
||||||
|
|
||||||
|
# USB Interface selector (production FT2232H / premium FT601)
|
||||||
|
self._usb_iface_var = tk.StringVar(value="FT2232H (Production)")
|
||||||
|
self.cmb_usb_iface = ttk.Combobox(
|
||||||
|
top, textvariable=self._usb_iface_var,
|
||||||
|
values=["FT2232H (Production)", "FT601 (Premium)"],
|
||||||
|
state="readonly", width=20,
|
||||||
|
)
|
||||||
|
self.cmb_usb_iface.pack(side="right", padx=4)
|
||||||
|
ttk.Label(top, text="USB:", font=("Menlo", 10)).pack(side="right")
|
||||||
|
|
||||||
self.btn_record = ttk.Button(top, text="Record", command=self._on_record)
|
self.btn_record = ttk.Button(top, text="Record", command=self._on_record)
|
||||||
self.btn_record.pack(side="right", padx=4)
|
self.btn_record.pack(side="right", padx=4)
|
||||||
|
|
||||||
@@ -515,9 +527,8 @@ class RadarDashboard:
|
|||||||
|
|
||||||
def _build_display_tab(self, parent):
|
def _build_display_tab(self, parent):
|
||||||
# Compute physical axis limits
|
# Compute physical axis limits
|
||||||
range_res = self.C / (2.0 * self.BANDWIDTH) # ~0.3 m per FFT bin
|
# Bin spacing = c / (2 * Fs_ddc) for matched-filter processing.
|
||||||
# After decimation 1024→64, each range bin = 16 FFT bins
|
range_per_bin = self.C / (2.0 * self.SAMPLE_RATE) * 16 # ~24 m
|
||||||
range_per_bin = range_res * 16
|
|
||||||
max_range = range_per_bin * NUM_RANGE_BINS
|
max_range = range_per_bin * NUM_RANGE_BINS
|
||||||
|
|
||||||
doppler_bin_lo = 0
|
doppler_bin_lo = 0
|
||||||
@@ -1018,15 +1029,17 @@ class RadarDashboard:
|
|||||||
|
|
||||||
# ------------------------------------------------------------ Actions
|
# ------------------------------------------------------------ Actions
|
||||||
def _on_connect(self):
|
def _on_connect(self):
|
||||||
if self.conn.is_open:
|
if self.conn is not None and self.conn.is_open:
|
||||||
# Disconnect
|
# Disconnect
|
||||||
if self._acq_thread is not None:
|
if self._acq_thread is not None:
|
||||||
self._acq_thread.stop()
|
self._acq_thread.stop()
|
||||||
self._acq_thread.join(timeout=2)
|
self._acq_thread.join(timeout=2)
|
||||||
self._acq_thread = None
|
self._acq_thread = None
|
||||||
self.conn.close()
|
self.conn.close()
|
||||||
|
self.conn = None
|
||||||
self.lbl_status.config(text="DISCONNECTED", foreground=RED)
|
self.lbl_status.config(text="DISCONNECTED", foreground=RED)
|
||||||
self.btn_connect.config(text="Connect")
|
self.btn_connect.config(text="Connect")
|
||||||
|
self.cmb_usb_iface.config(state="readonly")
|
||||||
log.info("Disconnected")
|
log.info("Disconnected")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1036,6 +1049,16 @@ class RadarDashboard:
|
|||||||
if self._replay_active:
|
if self._replay_active:
|
||||||
self._replay_stop()
|
self._replay_stop()
|
||||||
|
|
||||||
|
# Create connection based on USB Interface selector
|
||||||
|
iface = self._usb_iface_var.get()
|
||||||
|
if "FT601" in iface:
|
||||||
|
self.conn = FT601Connection(mock=self._mock)
|
||||||
|
else:
|
||||||
|
self.conn = FT2232HConnection(mock=self._mock)
|
||||||
|
|
||||||
|
# Disable interface selector while connecting/connected
|
||||||
|
self.cmb_usb_iface.config(state="disabled")
|
||||||
|
|
||||||
# Open connection in a background thread to avoid blocking the GUI
|
# Open connection in a background thread to avoid blocking the GUI
|
||||||
self.lbl_status.config(text="CONNECTING...", foreground=YELLOW)
|
self.lbl_status.config(text="CONNECTING...", foreground=YELLOW)
|
||||||
self.btn_connect.config(state="disabled")
|
self.btn_connect.config(state="disabled")
|
||||||
@@ -1062,6 +1085,8 @@ class RadarDashboard:
|
|||||||
else:
|
else:
|
||||||
self.lbl_status.config(text="CONNECT FAILED", foreground=RED)
|
self.lbl_status.config(text="CONNECT FAILED", foreground=RED)
|
||||||
self.btn_connect.config(text="Connect")
|
self.btn_connect.config(text="Connect")
|
||||||
|
self.cmb_usb_iface.config(state="readonly")
|
||||||
|
self.conn = None
|
||||||
|
|
||||||
def _on_record(self):
|
def _on_record(self):
|
||||||
if self.recorder.recording:
|
if self.recorder.recording:
|
||||||
@@ -1110,6 +1135,9 @@ class RadarDashboard:
|
|||||||
f"Opcode 0x{opcode:02X} is hardware-only (ignored in replay)"))
|
f"Opcode 0x{opcode:02X} is hardware-only (ignored in replay)"))
|
||||||
return
|
return
|
||||||
cmd = RadarProtocol.build_command(opcode, value)
|
cmd = RadarProtocol.build_command(opcode, value)
|
||||||
|
if self.conn is None:
|
||||||
|
log.warning("No connection — command not sent")
|
||||||
|
return
|
||||||
ok = self.conn.write(cmd)
|
ok = self.conn.write(cmd)
|
||||||
log.info(f"CMD 0x{opcode:02X} val={value} ({'OK' if ok else 'FAIL'})")
|
log.info(f"CMD 0x{opcode:02X} val={value} ({'OK' if ok else 'FAIL'})")
|
||||||
|
|
||||||
@@ -1148,7 +1176,7 @@ class RadarDashboard:
|
|||||||
if self._replay_active or self._replay_ctrl is not None:
|
if self._replay_active or self._replay_ctrl is not None:
|
||||||
self._replay_stop()
|
self._replay_stop()
|
||||||
if self._acq_thread is not None:
|
if self._acq_thread is not None:
|
||||||
if self.conn.is_open:
|
if self.conn is not None and self.conn.is_open:
|
||||||
self._on_connect() # disconnect
|
self._on_connect() # disconnect
|
||||||
else:
|
else:
|
||||||
# Connection dropped unexpectedly — just clean up the thread
|
# Connection dropped unexpectedly — just clean up the thread
|
||||||
@@ -1547,17 +1575,17 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.live:
|
if args.live:
|
||||||
conn = FT2232HConnection(mock=False)
|
mock = False
|
||||||
mode_str = "LIVE"
|
mode_str = "LIVE"
|
||||||
else:
|
else:
|
||||||
conn = FT2232HConnection(mock=True)
|
mock = True
|
||||||
mode_str = "MOCK"
|
mode_str = "MOCK"
|
||||||
|
|
||||||
recorder = DataRecorder()
|
recorder = DataRecorder()
|
||||||
|
|
||||||
root = tk.Tk()
|
root = tk.Tk()
|
||||||
|
|
||||||
dashboard = RadarDashboard(root, conn, recorder, device_index=args.device)
|
dashboard = RadarDashboard(root, mock, recorder, device_index=args.device)
|
||||||
|
|
||||||
if args.record:
|
if args.record:
|
||||||
filepath = os.path.join(
|
filepath = os.path.join(
|
||||||
@@ -1582,8 +1610,8 @@ def main():
|
|||||||
if dashboard._acq_thread is not None:
|
if dashboard._acq_thread is not None:
|
||||||
dashboard._acq_thread.stop()
|
dashboard._acq_thread.stop()
|
||||||
dashboard._acq_thread.join(timeout=2)
|
dashboard._acq_thread.join(timeout=2)
|
||||||
if conn.is_open:
|
if dashboard.conn is not None and dashboard.conn.is_open:
|
||||||
conn.close()
|
dashboard.conn.close()
|
||||||
if recorder.recording:
|
if recorder.recording:
|
||||||
recorder.stop()
|
recorder.stop()
|
||||||
root.destroy()
|
root.destroy()
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# DEPRECATED: GUI V6 Demo is superseded by GUI_V65_Tk and V7.
|
||||||
|
# This file is retained for reference only. Do not use for new development.
|
||||||
|
# Removal planned for next major release.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Radar System GUI - Fully Functional Demo Version
|
Radar System GUI - Fully Functional Demo Version
|
||||||
All buttons work, simulated radar data is generated in real-time
|
All buttons work, simulated radar data is generated in real-time
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ GUI_V4 ==> Added pitch correction
|
|||||||
|
|
||||||
GUI_V5 ==> Added Mercury Color
|
GUI_V5 ==> Added Mercury Color
|
||||||
|
|
||||||
GUI_V6 ==> Added USB3 FT601 support
|
GUI_V6 ==> Added USB3 FT601 support [DEPRECATED — superseded by V65/V7]
|
||||||
|
|
||||||
GUI_V65_Tk ==> Board bring-up dashboard (FT2232H reader, real-time R-D heatmap, CFAR overlay, waterfall, host commands, HDF5 recording, replay, demo mode)
|
GUI_V65_Tk ==> Board bring-up dashboard (FT2232H reader, real-time R-D heatmap, CFAR overlay, waterfall, host commands, HDF5 recording, replay, demo mode)
|
||||||
radar_protocol ==> Protocol layer (packet parsing, command building, FT2232H connection, data recorder, acquisition thread)
|
radar_protocol ==> Protocol layer (packet parsing, command building, FT2232H connection, data recorder, acquisition thread)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Pure-logic module for USB packet parsing and command building.
|
|||||||
No GUI dependencies — safe to import from tests and headless scripts.
|
No GUI dependencies — safe to import from tests and headless scripts.
|
||||||
|
|
||||||
USB Interface: FT2232H USB 2.0 (8-bit, 50T production board) via pyftdi
|
USB Interface: FT2232H USB 2.0 (8-bit, 50T production board) via pyftdi
|
||||||
|
FT601 USB 3.0 (32-bit, 200T premium board) via ftd3xx
|
||||||
|
|
||||||
USB Packet Protocol (11-byte):
|
USB Packet Protocol (11-byte):
|
||||||
TX (FPGA→Host):
|
TX (FPGA→Host):
|
||||||
@@ -22,7 +23,7 @@ import queue
|
|||||||
import logging
|
import logging
|
||||||
import contextlib
|
import contextlib
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any
|
from typing import Any, ClassVar
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
|
||||||
|
|
||||||
@@ -200,7 +201,9 @@ class RadarProtocol:
|
|||||||
range_i = _to_signed16(struct.unpack_from(">H", raw, 3)[0])
|
range_i = _to_signed16(struct.unpack_from(">H", raw, 3)[0])
|
||||||
doppler_i = _to_signed16(struct.unpack_from(">H", raw, 5)[0])
|
doppler_i = _to_signed16(struct.unpack_from(">H", raw, 5)[0])
|
||||||
doppler_q = _to_signed16(struct.unpack_from(">H", raw, 7)[0])
|
doppler_q = _to_signed16(struct.unpack_from(">H", raw, 7)[0])
|
||||||
detection = raw[9] & 0x01
|
det_byte = raw[9]
|
||||||
|
detection = det_byte & 0x01
|
||||||
|
frame_start = (det_byte >> 7) & 0x01
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"range_i": range_i,
|
"range_i": range_i,
|
||||||
@@ -208,6 +211,7 @@ class RadarProtocol:
|
|||||||
"doppler_i": doppler_i,
|
"doppler_i": doppler_i,
|
||||||
"doppler_q": doppler_q,
|
"doppler_q": doppler_q,
|
||||||
"detection": detection,
|
"detection": detection,
|
||||||
|
"frame_start": frame_start,
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -433,7 +437,191 @@ class FT2232HConnection:
|
|||||||
pkt += struct.pack(">h", np.clip(range_i, -32768, 32767))
|
pkt += struct.pack(">h", np.clip(range_i, -32768, 32767))
|
||||||
pkt += struct.pack(">h", np.clip(dop_i, -32768, 32767))
|
pkt += struct.pack(">h", np.clip(dop_i, -32768, 32767))
|
||||||
pkt += struct.pack(">h", np.clip(dop_q, -32768, 32767))
|
pkt += struct.pack(">h", np.clip(dop_q, -32768, 32767))
|
||||||
pkt.append(detection & 0x01)
|
# Bit 7 = frame_start (sample_counter == 0), bit 0 = detection
|
||||||
|
det_byte = (detection & 0x01) | (0x80 if idx == 0 else 0x00)
|
||||||
|
pkt.append(det_byte)
|
||||||
|
pkt.append(FOOTER_BYTE)
|
||||||
|
|
||||||
|
buf += pkt
|
||||||
|
|
||||||
|
self._mock_seq_idx = (start_idx + num_packets) % NUM_CELLS
|
||||||
|
return bytes(buf)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# FT601 USB 3.0 Connection (premium board only)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Optional ftd3xx import (FTDI's proprietary driver for FT60x USB 3.0 chips).
|
||||||
|
# pyftdi does NOT support FT601 — it only handles USB 2.0 chips (FT232H, etc.)
|
||||||
|
try:
|
||||||
|
import ftd3xx # type: ignore[import-untyped]
|
||||||
|
FTD3XX_AVAILABLE = True
|
||||||
|
_Ftd3xxError: type = ftd3xx.FTD3XXError # type: ignore[attr-defined]
|
||||||
|
except ImportError:
|
||||||
|
FTD3XX_AVAILABLE = False
|
||||||
|
_Ftd3xxError = OSError # fallback for type-checking; never raised
|
||||||
|
|
||||||
|
|
||||||
|
class FT601Connection:
|
||||||
|
"""
|
||||||
|
FT601 USB 3.0 SuperSpeed FIFO bridge — premium board only.
|
||||||
|
|
||||||
|
The FT601 has a 32-bit data bus and runs at 100 MHz.
|
||||||
|
VID:PID = 0x0403:0x6030 or 0x6031 (FTDI FT60x).
|
||||||
|
|
||||||
|
Requires the ``ftd3xx`` library (``pip install ftd3xx`` on Windows,
|
||||||
|
or ``libft60x`` on Linux). This is FTDI's proprietary USB 3.0 driver;
|
||||||
|
``pyftdi`` only supports USB 2.0 and will NOT work with FT601.
|
||||||
|
|
||||||
|
Public contract matches FT2232HConnection so callers can swap freely.
|
||||||
|
"""
|
||||||
|
|
||||||
|
VID = 0x0403
|
||||||
|
PID_LIST: ClassVar[list[int]] = [0x6030, 0x6031]
|
||||||
|
|
||||||
|
def __init__(self, mock: bool = True):
|
||||||
|
self._mock = mock
|
||||||
|
self._dev = None
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self.is_open = False
|
||||||
|
# Mock state (reuses same synthetic data pattern)
|
||||||
|
self._mock_frame_num = 0
|
||||||
|
self._mock_rng = np.random.RandomState(42)
|
||||||
|
|
||||||
|
def open(self, device_index: int = 0) -> bool:
|
||||||
|
if self._mock:
|
||||||
|
self.is_open = True
|
||||||
|
log.info("FT601 mock device opened (no hardware)")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not FTD3XX_AVAILABLE:
|
||||||
|
log.error(
|
||||||
|
"ftd3xx library required for FT601 hardware — "
|
||||||
|
"install with: pip install ftd3xx"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._dev = ftd3xx.create(device_index, ftd3xx.OPEN_BY_INDEX)
|
||||||
|
if self._dev is None:
|
||||||
|
log.error("No FT601 device found at index %d", device_index)
|
||||||
|
return False
|
||||||
|
# Verify chip configuration — only reconfigure if needed.
|
||||||
|
# setChipConfiguration triggers USB re-enumeration, which
|
||||||
|
# invalidates the device handle and requires a re-open cycle.
|
||||||
|
cfg = self._dev.getChipConfiguration()
|
||||||
|
needs_reconfig = (
|
||||||
|
cfg.FIFOMode != 0 # 245 FIFO mode
|
||||||
|
or cfg.ChannelConfig != 0 # 1 channel, 32-bit
|
||||||
|
or cfg.OptionalFeatureSupport != 0
|
||||||
|
)
|
||||||
|
if needs_reconfig:
|
||||||
|
cfg.FIFOMode = 0
|
||||||
|
cfg.ChannelConfig = 0
|
||||||
|
cfg.OptionalFeatureSupport = 0
|
||||||
|
self._dev.setChipConfiguration(cfg)
|
||||||
|
# Device re-enumerates — close stale handle, wait, re-open
|
||||||
|
self._dev.close()
|
||||||
|
self._dev = None
|
||||||
|
import time
|
||||||
|
time.sleep(2.0) # wait for USB re-enumeration
|
||||||
|
self._dev = ftd3xx.create(device_index, ftd3xx.OPEN_BY_INDEX)
|
||||||
|
if self._dev is None:
|
||||||
|
log.error("FT601 not found after reconfiguration")
|
||||||
|
return False
|
||||||
|
log.info("FT601 reconfigured and re-opened (index %d)", device_index)
|
||||||
|
self.is_open = True
|
||||||
|
log.info("FT601 device opened (index %d)", device_index)
|
||||||
|
return True
|
||||||
|
except (OSError, _Ftd3xxError) as e:
|
||||||
|
log.error("FT601 open failed: %s", e)
|
||||||
|
self._dev = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self._dev is not None:
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
self._dev.close()
|
||||||
|
self._dev = None
|
||||||
|
self.is_open = False
|
||||||
|
|
||||||
|
def read(self, size: int = 4096) -> bytes | None:
|
||||||
|
"""Read raw bytes from FT601. Returns None on error/timeout."""
|
||||||
|
if not self.is_open:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self._mock:
|
||||||
|
return self._mock_read(size)
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
try:
|
||||||
|
data = self._dev.readPipe(0x82, size, raw=True)
|
||||||
|
return bytes(data) if data else None
|
||||||
|
except (OSError, _Ftd3xxError) as e:
|
||||||
|
log.error("FT601 read error: %s", e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def write(self, data: bytes) -> bool:
|
||||||
|
"""Write raw bytes to FT601. Data must be 4-byte aligned for 32-bit bus."""
|
||||||
|
if not self.is_open:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self._mock:
|
||||||
|
log.info(f"FT601 mock write: {data.hex()}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Pad to 4-byte alignment (FT601 32-bit bus requirement).
|
||||||
|
# NOTE: Radar commands are already 4 bytes, so this should be a no-op.
|
||||||
|
remainder = len(data) % 4
|
||||||
|
if remainder:
|
||||||
|
data = data + b"\x00" * (4 - remainder)
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
try:
|
||||||
|
written = self._dev.writePipe(0x02, data, raw=True)
|
||||||
|
return written == len(data)
|
||||||
|
except (OSError, _Ftd3xxError) as e:
|
||||||
|
log.error("FT601 write error: %s", e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _mock_read(self, size: int) -> bytes:
|
||||||
|
"""Generate synthetic radar packets (same pattern as FT2232H mock)."""
|
||||||
|
time.sleep(0.05)
|
||||||
|
self._mock_frame_num += 1
|
||||||
|
|
||||||
|
buf = bytearray()
|
||||||
|
num_packets = min(NUM_CELLS, size // DATA_PACKET_SIZE)
|
||||||
|
start_idx = getattr(self, "_mock_seq_idx", 0)
|
||||||
|
|
||||||
|
for n in range(num_packets):
|
||||||
|
idx = (start_idx + n) % NUM_CELLS
|
||||||
|
rbin = idx // NUM_DOPPLER_BINS
|
||||||
|
dbin = idx % NUM_DOPPLER_BINS
|
||||||
|
|
||||||
|
range_i = int(self._mock_rng.normal(0, 100))
|
||||||
|
range_q = int(self._mock_rng.normal(0, 100))
|
||||||
|
if abs(rbin - 20) < 3:
|
||||||
|
range_i += 5000
|
||||||
|
range_q += 3000
|
||||||
|
|
||||||
|
dop_i = int(self._mock_rng.normal(0, 50))
|
||||||
|
dop_q = int(self._mock_rng.normal(0, 50))
|
||||||
|
if abs(rbin - 20) < 3 and abs(dbin - 8) < 2:
|
||||||
|
dop_i += 8000
|
||||||
|
dop_q += 4000
|
||||||
|
|
||||||
|
detection = 1 if (abs(rbin - 20) < 2 and abs(dbin - 8) < 2) else 0
|
||||||
|
|
||||||
|
pkt = bytearray()
|
||||||
|
pkt.append(HEADER_BYTE)
|
||||||
|
pkt += struct.pack(">h", np.clip(range_q, -32768, 32767))
|
||||||
|
pkt += struct.pack(">h", np.clip(range_i, -32768, 32767))
|
||||||
|
pkt += struct.pack(">h", np.clip(dop_i, -32768, 32767))
|
||||||
|
pkt += struct.pack(">h", np.clip(dop_q, -32768, 32767))
|
||||||
|
# Bit 7 = frame_start (sample_counter == 0), bit 0 = detection
|
||||||
|
det_byte = (detection & 0x01) | (0x80 if idx == 0 else 0x00)
|
||||||
|
pkt.append(det_byte)
|
||||||
pkt.append(FOOTER_BYTE)
|
pkt.append(FOOTER_BYTE)
|
||||||
|
|
||||||
buf += pkt
|
buf += pkt
|
||||||
@@ -600,6 +788,12 @@ class RadarAcquisition(threading.Thread):
|
|||||||
if sample.get("detection", 0):
|
if sample.get("detection", 0):
|
||||||
self._frame.detections[rbin, dbin] = 1
|
self._frame.detections[rbin, dbin] = 1
|
||||||
self._frame.detection_count += 1
|
self._frame.detection_count += 1
|
||||||
|
# Accumulate FPGA range profile data (matched-filter output)
|
||||||
|
# Each sample carries the range_i/range_q for this range bin.
|
||||||
|
# Accumulate magnitude across Doppler bins for the range profile.
|
||||||
|
ri = int(sample.get("range_i", 0))
|
||||||
|
rq = int(sample.get("range_q", 0))
|
||||||
|
self._frame.range_profile[rbin] += abs(ri) + abs(rq)
|
||||||
|
|
||||||
self._sample_idx += 1
|
self._sample_idx += 1
|
||||||
|
|
||||||
@@ -607,11 +801,11 @@ class RadarAcquisition(threading.Thread):
|
|||||||
self._finalize_frame()
|
self._finalize_frame()
|
||||||
|
|
||||||
def _finalize_frame(self):
|
def _finalize_frame(self):
|
||||||
"""Complete frame: compute range profile, push to queue, record."""
|
"""Complete frame: push to queue, record."""
|
||||||
self._frame.timestamp = time.time()
|
self._frame.timestamp = time.time()
|
||||||
self._frame.frame_number = self._frame_num
|
self._frame.frame_number = self._frame_num
|
||||||
# Range profile = sum of magnitude across Doppler bins
|
# range_profile is already accumulated from FPGA range_i/range_q
|
||||||
self._frame.range_profile = np.sum(self._frame.magnitude, axis=1)
|
# data in _ingest_sample(). No need to synthesize from doppler magnitude.
|
||||||
|
|
||||||
# Push to display queue (drop old if backed up)
|
# Push to display queue (drop old if backed up)
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import unittest
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from radar_protocol import (
|
from radar_protocol import (
|
||||||
RadarProtocol, FT2232HConnection, DataRecorder, RadarAcquisition,
|
RadarProtocol, FT2232HConnection, FT601Connection, DataRecorder, RadarAcquisition,
|
||||||
RadarFrame, StatusResponse, Opcode,
|
RadarFrame, StatusResponse, Opcode,
|
||||||
HEADER_BYTE, FOOTER_BYTE, STATUS_HEADER_BYTE,
|
HEADER_BYTE, FOOTER_BYTE, STATUS_HEADER_BYTE,
|
||||||
NUM_RANGE_BINS, NUM_DOPPLER_BINS,
|
NUM_RANGE_BINS, NUM_DOPPLER_BINS,
|
||||||
@@ -312,6 +312,61 @@ class TestFT2232HConnection(unittest.TestCase):
|
|||||||
self.assertFalse(conn.write(b"\x00\x00\x00\x00"))
|
self.assertFalse(conn.write(b"\x00\x00\x00\x00"))
|
||||||
|
|
||||||
|
|
||||||
|
class TestFT601Connection(unittest.TestCase):
|
||||||
|
"""Test mock FT601 connection (mirrors FT2232H tests)."""
|
||||||
|
|
||||||
|
def test_mock_open_close(self):
|
||||||
|
conn = FT601Connection(mock=True)
|
||||||
|
self.assertTrue(conn.open())
|
||||||
|
self.assertTrue(conn.is_open)
|
||||||
|
conn.close()
|
||||||
|
self.assertFalse(conn.is_open)
|
||||||
|
|
||||||
|
def test_mock_read_returns_data(self):
|
||||||
|
conn = FT601Connection(mock=True)
|
||||||
|
conn.open()
|
||||||
|
data = conn.read(4096)
|
||||||
|
self.assertIsNotNone(data)
|
||||||
|
self.assertGreater(len(data), 0)
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def test_mock_read_contains_valid_packets(self):
|
||||||
|
"""Mock data should contain parseable data packets."""
|
||||||
|
conn = FT601Connection(mock=True)
|
||||||
|
conn.open()
|
||||||
|
raw = conn.read(4096)
|
||||||
|
packets = RadarProtocol.find_packet_boundaries(raw)
|
||||||
|
self.assertGreater(len(packets), 0)
|
||||||
|
for start, end, ptype in packets:
|
||||||
|
if ptype == "data":
|
||||||
|
result = RadarProtocol.parse_data_packet(raw[start:end])
|
||||||
|
self.assertIsNotNone(result)
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def test_mock_write(self):
|
||||||
|
conn = FT601Connection(mock=True)
|
||||||
|
conn.open()
|
||||||
|
cmd = RadarProtocol.build_command(0x01, 1)
|
||||||
|
self.assertTrue(conn.write(cmd))
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def test_write_pads_to_4_bytes(self):
|
||||||
|
"""FT601 write() should pad data to 4-byte alignment."""
|
||||||
|
conn = FT601Connection(mock=True)
|
||||||
|
conn.open()
|
||||||
|
# 3-byte payload should be padded internally (no error)
|
||||||
|
self.assertTrue(conn.write(b"\x01\x02\x03"))
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def test_read_when_closed(self):
|
||||||
|
conn = FT601Connection(mock=True)
|
||||||
|
self.assertIsNone(conn.read())
|
||||||
|
|
||||||
|
def test_write_when_closed(self):
|
||||||
|
conn = FT601Connection(mock=True)
|
||||||
|
self.assertFalse(conn.write(b"\x00\x00\x00\x00"))
|
||||||
|
|
||||||
|
|
||||||
class TestDataRecorder(unittest.TestCase):
|
class TestDataRecorder(unittest.TestCase):
|
||||||
"""Test HDF5 recording (skipped if h5py not available)."""
|
"""Test HDF5 recording (skipped if h5py not available)."""
|
||||||
|
|
||||||
|
|||||||
@@ -65,9 +65,9 @@ class TestRadarSettings(unittest.TestCase):
|
|||||||
|
|
||||||
def test_defaults(self):
|
def test_defaults(self):
|
||||||
s = _models().RadarSettings()
|
s = _models().RadarSettings()
|
||||||
self.assertEqual(s.system_frequency, 10e9)
|
self.assertEqual(s.system_frequency, 10.5e9)
|
||||||
self.assertEqual(s.coverage_radius, 50000)
|
self.assertEqual(s.coverage_radius, 1536)
|
||||||
self.assertEqual(s.max_distance, 50000)
|
self.assertEqual(s.max_distance, 1536)
|
||||||
|
|
||||||
|
|
||||||
class TestGPSData(unittest.TestCase):
|
class TestGPSData(unittest.TestCase):
|
||||||
@@ -425,26 +425,28 @@ class TestWaveformConfig(unittest.TestCase):
|
|||||||
def test_defaults(self):
|
def test_defaults(self):
|
||||||
from v7.models import WaveformConfig
|
from v7.models import WaveformConfig
|
||||||
wc = WaveformConfig()
|
wc = WaveformConfig()
|
||||||
self.assertEqual(wc.sample_rate_hz, 4e6)
|
self.assertEqual(wc.sample_rate_hz, 100e6)
|
||||||
self.assertEqual(wc.bandwidth_hz, 500e6)
|
self.assertEqual(wc.bandwidth_hz, 20e6)
|
||||||
self.assertEqual(wc.chirp_duration_s, 300e-6)
|
self.assertEqual(wc.chirp_duration_s, 30e-6)
|
||||||
self.assertEqual(wc.center_freq_hz, 10.525e9)
|
self.assertEqual(wc.pri_s, 167e-6)
|
||||||
|
self.assertEqual(wc.center_freq_hz, 10.5e9)
|
||||||
self.assertEqual(wc.n_range_bins, 64)
|
self.assertEqual(wc.n_range_bins, 64)
|
||||||
self.assertEqual(wc.n_doppler_bins, 32)
|
self.assertEqual(wc.n_doppler_bins, 32)
|
||||||
|
self.assertEqual(wc.chirps_per_subframe, 16)
|
||||||
self.assertEqual(wc.fft_size, 1024)
|
self.assertEqual(wc.fft_size, 1024)
|
||||||
self.assertEqual(wc.decimation_factor, 16)
|
self.assertEqual(wc.decimation_factor, 16)
|
||||||
|
|
||||||
def test_range_resolution(self):
|
def test_range_resolution(self):
|
||||||
"""range_resolution_m should be ~5.62 m/bin with ADI defaults."""
|
"""range_resolution_m should be ~23.98 m/bin (matched filter, 100 MSPS)."""
|
||||||
from v7.models import WaveformConfig
|
from v7.models import WaveformConfig
|
||||||
wc = WaveformConfig()
|
wc = WaveformConfig()
|
||||||
self.assertAlmostEqual(wc.range_resolution_m, 5.621, places=1)
|
self.assertAlmostEqual(wc.range_resolution_m, 23.983, places=1)
|
||||||
|
|
||||||
def test_velocity_resolution(self):
|
def test_velocity_resolution(self):
|
||||||
"""velocity_resolution_mps should be ~1.484 m/s/bin."""
|
"""velocity_resolution_mps should be ~5.34 m/s/bin (PRI=167us, 16 chirps)."""
|
||||||
from v7.models import WaveformConfig
|
from v7.models import WaveformConfig
|
||||||
wc = WaveformConfig()
|
wc = WaveformConfig()
|
||||||
self.assertAlmostEqual(wc.velocity_resolution_mps, 1.484, places=2)
|
self.assertAlmostEqual(wc.velocity_resolution_mps, 5.343, places=1)
|
||||||
|
|
||||||
def test_max_range(self):
|
def test_max_range(self):
|
||||||
"""max_range_m = range_resolution * n_range_bins."""
|
"""max_range_m = range_resolution * n_range_bins."""
|
||||||
@@ -466,7 +468,7 @@ class TestWaveformConfig(unittest.TestCase):
|
|||||||
"""Non-default parameters correctly change derived values."""
|
"""Non-default parameters correctly change derived values."""
|
||||||
from v7.models import WaveformConfig
|
from v7.models import WaveformConfig
|
||||||
wc1 = WaveformConfig()
|
wc1 = WaveformConfig()
|
||||||
wc2 = WaveformConfig(bandwidth_hz=1e9) # double BW → halve range res
|
wc2 = WaveformConfig(sample_rate_hz=200e6) # double Fs → halve range bin
|
||||||
self.assertAlmostEqual(wc2.range_resolution_m, wc1.range_resolution_m / 2, places=2)
|
self.assertAlmostEqual(wc2.range_resolution_m, wc1.range_resolution_m / 2, places=2)
|
||||||
|
|
||||||
def test_zero_center_freq_velocity(self):
|
def test_zero_center_freq_velocity(self):
|
||||||
@@ -925,9 +927,9 @@ class TestExtractTargetsFromFrame(unittest.TestCase):
|
|||||||
"""Detection at range bin 10 → range = 10 * range_resolution."""
|
"""Detection at range bin 10 → range = 10 * range_resolution."""
|
||||||
from v7.processing import extract_targets_from_frame
|
from v7.processing import extract_targets_from_frame
|
||||||
frame = self._make_frame(det_cells=[(10, 16)]) # dbin=16 = center → vel=0
|
frame = self._make_frame(det_cells=[(10, 16)]) # dbin=16 = center → vel=0
|
||||||
targets = extract_targets_from_frame(frame, range_resolution=5.621)
|
targets = extract_targets_from_frame(frame, range_resolution=23.983)
|
||||||
self.assertEqual(len(targets), 1)
|
self.assertEqual(len(targets), 1)
|
||||||
self.assertAlmostEqual(targets[0].range, 10 * 5.621, places=2)
|
self.assertAlmostEqual(targets[0].range, 10 * 23.983, places=1)
|
||||||
self.assertAlmostEqual(targets[0].velocity, 0.0, places=2)
|
self.assertAlmostEqual(targets[0].velocity, 0.0, places=2)
|
||||||
|
|
||||||
def test_velocity_sign(self):
|
def test_velocity_sign(self):
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from .models import (
|
|||||||
# Hardware interfaces — production protocol via radar_protocol.py
|
# Hardware interfaces — production protocol via radar_protocol.py
|
||||||
from .hardware import (
|
from .hardware import (
|
||||||
FT2232HConnection,
|
FT2232HConnection,
|
||||||
|
FT601Connection,
|
||||||
RadarProtocol,
|
RadarProtocol,
|
||||||
Opcode,
|
Opcode,
|
||||||
RadarAcquisition,
|
RadarAcquisition,
|
||||||
@@ -89,7 +90,7 @@ __all__ = [ # noqa: RUF022
|
|||||||
"USB_AVAILABLE", "FTDI_AVAILABLE", "SCIPY_AVAILABLE",
|
"USB_AVAILABLE", "FTDI_AVAILABLE", "SCIPY_AVAILABLE",
|
||||||
"SKLEARN_AVAILABLE", "FILTERPY_AVAILABLE",
|
"SKLEARN_AVAILABLE", "FILTERPY_AVAILABLE",
|
||||||
# hardware — production FPGA protocol
|
# hardware — production FPGA protocol
|
||||||
"FT2232HConnection", "RadarProtocol", "Opcode",
|
"FT2232HConnection", "FT601Connection", "RadarProtocol", "Opcode",
|
||||||
"RadarAcquisition", "RadarFrame", "StatusResponse", "DataRecorder",
|
"RadarAcquisition", "RadarFrame", "StatusResponse", "DataRecorder",
|
||||||
"STM32USBInterface",
|
"STM32USBInterface",
|
||||||
# processing
|
# processing
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ RadarDashboard is a QMainWindow with six tabs:
|
|||||||
6. Settings — Host-side DSP parameters + About section
|
6. Settings — Host-side DSP parameters + About section
|
||||||
|
|
||||||
Uses production radar_protocol.py for all FPGA communication:
|
Uses production radar_protocol.py for all FPGA communication:
|
||||||
- FT2232HConnection for real hardware
|
- FT2232HConnection for production board (FT2232H USB 2.0)
|
||||||
|
- FT601Connection for premium board (FT601 USB 3.0) — selectable from GUI
|
||||||
- Unified replay via SoftwareFPGA + ReplayEngine + ReplayWorker
|
- Unified replay via SoftwareFPGA + ReplayEngine + ReplayWorker
|
||||||
- Mock mode (FT2232HConnection(mock=True)) for development
|
- Mock mode (FT2232HConnection(mock=True)) for development
|
||||||
|
|
||||||
The old STM32 magic-packet start flow has been removed. FPGA registers
|
The old STM32 magic-packet start flow has been removed. FPGA registers
|
||||||
are controlled directly via 4-byte {opcode, addr, value_hi, value_lo}
|
are controlled directly via 4-byte {opcode, addr, value_hi, value_lo}
|
||||||
commands sent over FT2232H.
|
commands sent over FT2232H or FT601.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -55,6 +56,7 @@ from .models import (
|
|||||||
)
|
)
|
||||||
from .hardware import (
|
from .hardware import (
|
||||||
FT2232HConnection,
|
FT2232HConnection,
|
||||||
|
FT601Connection,
|
||||||
RadarProtocol,
|
RadarProtocol,
|
||||||
RadarFrame,
|
RadarFrame,
|
||||||
StatusResponse,
|
StatusResponse,
|
||||||
@@ -142,7 +144,7 @@ class RadarDashboard(QMainWindow):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Hardware interfaces — production protocol
|
# Hardware interfaces — production protocol
|
||||||
self._connection: FT2232HConnection | None = None
|
self._connection: FT2232HConnection | FT601Connection | None = None
|
||||||
self._stm32 = STM32USBInterface()
|
self._stm32 = STM32USBInterface()
|
||||||
self._recorder = DataRecorder()
|
self._recorder = DataRecorder()
|
||||||
|
|
||||||
@@ -364,7 +366,7 @@ class RadarDashboard(QMainWindow):
|
|||||||
# Row 0: connection mode + device combos + buttons
|
# Row 0: connection mode + device combos + buttons
|
||||||
ctrl_layout.addWidget(QLabel("Mode:"), 0, 0)
|
ctrl_layout.addWidget(QLabel("Mode:"), 0, 0)
|
||||||
self._mode_combo = QComboBox()
|
self._mode_combo = QComboBox()
|
||||||
self._mode_combo.addItems(["Mock", "Live FT2232H", "Replay"])
|
self._mode_combo.addItems(["Mock", "Live", "Replay"])
|
||||||
self._mode_combo.setCurrentIndex(0)
|
self._mode_combo.setCurrentIndex(0)
|
||||||
ctrl_layout.addWidget(self._mode_combo, 0, 1)
|
ctrl_layout.addWidget(self._mode_combo, 0, 1)
|
||||||
|
|
||||||
@@ -377,6 +379,13 @@ class RadarDashboard(QMainWindow):
|
|||||||
refresh_btn.clicked.connect(self._refresh_devices)
|
refresh_btn.clicked.connect(self._refresh_devices)
|
||||||
ctrl_layout.addWidget(refresh_btn, 0, 4)
|
ctrl_layout.addWidget(refresh_btn, 0, 4)
|
||||||
|
|
||||||
|
# USB Interface selector (production FT2232H / premium FT601)
|
||||||
|
ctrl_layout.addWidget(QLabel("USB Interface:"), 0, 5)
|
||||||
|
self._usb_iface_combo = QComboBox()
|
||||||
|
self._usb_iface_combo.addItems(["FT2232H (Production)", "FT601 (Premium)"])
|
||||||
|
self._usb_iface_combo.setCurrentIndex(0)
|
||||||
|
ctrl_layout.addWidget(self._usb_iface_combo, 0, 6)
|
||||||
|
|
||||||
self._start_btn = QPushButton("Start Radar")
|
self._start_btn = QPushButton("Start Radar")
|
||||||
self._start_btn.setStyleSheet(
|
self._start_btn.setStyleSheet(
|
||||||
f"QPushButton {{ background-color: {DARK_SUCCESS}; color: white; font-weight: bold; }}"
|
f"QPushButton {{ background-color: {DARK_SUCCESS}; color: white; font-weight: bold; }}"
|
||||||
@@ -1001,7 +1010,8 @@ class RadarDashboard(QMainWindow):
|
|||||||
self._conn_ft2232h = self._make_status_label("FT2232H")
|
self._conn_ft2232h = self._make_status_label("FT2232H")
|
||||||
self._conn_stm32 = self._make_status_label("STM32 USB")
|
self._conn_stm32 = self._make_status_label("STM32 USB")
|
||||||
|
|
||||||
conn_layout.addWidget(QLabel("FT2232H:"), 0, 0)
|
self._conn_usb_label = QLabel("USB Data:")
|
||||||
|
conn_layout.addWidget(self._conn_usb_label, 0, 0)
|
||||||
conn_layout.addWidget(self._conn_ft2232h, 0, 1)
|
conn_layout.addWidget(self._conn_ft2232h, 0, 1)
|
||||||
conn_layout.addWidget(QLabel("STM32 USB:"), 1, 0)
|
conn_layout.addWidget(QLabel("STM32 USB:"), 1, 0)
|
||||||
conn_layout.addWidget(self._conn_stm32, 1, 1)
|
conn_layout.addWidget(self._conn_stm32, 1, 1)
|
||||||
@@ -1167,7 +1177,7 @@ class RadarDashboard(QMainWindow):
|
|||||||
about_lbl = QLabel(
|
about_lbl = QLabel(
|
||||||
"<b>AERIS-10 Radar System V7</b><br>"
|
"<b>AERIS-10 Radar System V7</b><br>"
|
||||||
"PyQt6 Edition with Embedded Leaflet Map<br><br>"
|
"PyQt6 Edition with Embedded Leaflet Map<br><br>"
|
||||||
"<b>Data Interface:</b> FT2232H USB 2.0 (production protocol)<br>"
|
"<b>Data Interface:</b> FT2232H USB 2.0 (production) / FT601 USB 3.0 (premium)<br>"
|
||||||
"<b>FPGA Protocol:</b> 4-byte register commands, 0xAA/0xBB packets<br>"
|
"<b>FPGA Protocol:</b> 4-byte register commands, 0xAA/0xBB packets<br>"
|
||||||
"<b>Map:</b> OpenStreetMap + Leaflet.js<br>"
|
"<b>Map:</b> OpenStreetMap + Leaflet.js<br>"
|
||||||
"<b>Framework:</b> PyQt6 + QWebEngine<br>"
|
"<b>Framework:</b> PyQt6 + QWebEngine<br>"
|
||||||
@@ -1224,7 +1234,7 @@ class RadarDashboard(QMainWindow):
|
|||||||
# =====================================================================
|
# =====================================================================
|
||||||
|
|
||||||
def _send_fpga_cmd(self, opcode: int, value: int):
|
def _send_fpga_cmd(self, opcode: int, value: int):
|
||||||
"""Send a 4-byte register command to the FPGA via FT2232H."""
|
"""Send a 4-byte register command to the FPGA via USB (FT2232H or FT601)."""
|
||||||
if self._connection is None or not self._connection.is_open:
|
if self._connection is None or not self._connection.is_open:
|
||||||
logger.warning(f"Cannot send 0x{opcode:02X}={value}: no connection")
|
logger.warning(f"Cannot send 0x{opcode:02X}={value}: no connection")
|
||||||
return
|
return
|
||||||
@@ -1287,16 +1297,26 @@ class RadarDashboard(QMainWindow):
|
|||||||
|
|
||||||
if "Mock" in mode:
|
if "Mock" in mode:
|
||||||
self._replay_mode = False
|
self._replay_mode = False
|
||||||
self._connection = FT2232HConnection(mock=True)
|
iface = self._usb_iface_combo.currentText()
|
||||||
|
if "FT601" in iface:
|
||||||
|
self._connection = FT601Connection(mock=True)
|
||||||
|
else:
|
||||||
|
self._connection = FT2232HConnection(mock=True)
|
||||||
if not self._connection.open():
|
if not self._connection.open():
|
||||||
QMessageBox.critical(self, "Error", "Failed to open mock connection.")
|
QMessageBox.critical(self, "Error", "Failed to open mock connection.")
|
||||||
return
|
return
|
||||||
elif "Live" in mode:
|
elif "Live" in mode:
|
||||||
self._replay_mode = False
|
self._replay_mode = False
|
||||||
self._connection = FT2232HConnection(mock=False)
|
iface = self._usb_iface_combo.currentText()
|
||||||
|
if "FT601" in iface:
|
||||||
|
self._connection = FT601Connection(mock=False)
|
||||||
|
iface_name = "FT601"
|
||||||
|
else:
|
||||||
|
self._connection = FT2232HConnection(mock=False)
|
||||||
|
iface_name = "FT2232H"
|
||||||
if not self._connection.open():
|
if not self._connection.open():
|
||||||
QMessageBox.critical(self, "Error",
|
QMessageBox.critical(self, "Error",
|
||||||
"Failed to open FT2232H. Check USB connection.")
|
f"Failed to open {iface_name}. Check USB connection.")
|
||||||
return
|
return
|
||||||
elif "Replay" in mode:
|
elif "Replay" in mode:
|
||||||
self._replay_mode = True
|
self._replay_mode = True
|
||||||
@@ -1368,6 +1388,7 @@ class RadarDashboard(QMainWindow):
|
|||||||
self._start_btn.setEnabled(False)
|
self._start_btn.setEnabled(False)
|
||||||
self._stop_btn.setEnabled(True)
|
self._stop_btn.setEnabled(True)
|
||||||
self._mode_combo.setEnabled(False)
|
self._mode_combo.setEnabled(False)
|
||||||
|
self._usb_iface_combo.setEnabled(False)
|
||||||
self._demo_btn_main.setEnabled(False)
|
self._demo_btn_main.setEnabled(False)
|
||||||
self._demo_btn_map.setEnabled(False)
|
self._demo_btn_map.setEnabled(False)
|
||||||
n_frames = self._replay_engine.total_frames
|
n_frames = self._replay_engine.total_frames
|
||||||
@@ -1417,6 +1438,7 @@ class RadarDashboard(QMainWindow):
|
|||||||
self._start_btn.setEnabled(False)
|
self._start_btn.setEnabled(False)
|
||||||
self._stop_btn.setEnabled(True)
|
self._stop_btn.setEnabled(True)
|
||||||
self._mode_combo.setEnabled(False)
|
self._mode_combo.setEnabled(False)
|
||||||
|
self._usb_iface_combo.setEnabled(False)
|
||||||
self._demo_btn_main.setEnabled(False)
|
self._demo_btn_main.setEnabled(False)
|
||||||
self._demo_btn_map.setEnabled(False)
|
self._demo_btn_map.setEnabled(False)
|
||||||
self._status_label_main.setText(f"Status: Running ({mode})")
|
self._status_label_main.setText(f"Status: Running ({mode})")
|
||||||
@@ -1462,6 +1484,7 @@ class RadarDashboard(QMainWindow):
|
|||||||
self._start_btn.setEnabled(True)
|
self._start_btn.setEnabled(True)
|
||||||
self._stop_btn.setEnabled(False)
|
self._stop_btn.setEnabled(False)
|
||||||
self._mode_combo.setEnabled(True)
|
self._mode_combo.setEnabled(True)
|
||||||
|
self._usb_iface_combo.setEnabled(True)
|
||||||
self._demo_btn_main.setEnabled(True)
|
self._demo_btn_main.setEnabled(True)
|
||||||
self._demo_btn_map.setEnabled(True)
|
self._demo_btn_map.setEnabled(True)
|
||||||
self._status_label_main.setText("Status: Radar stopped")
|
self._status_label_main.setText("Status: Radar stopped")
|
||||||
@@ -1954,6 +1977,12 @@ class RadarDashboard(QMainWindow):
|
|||||||
self._set_conn_indicator(self._conn_ft2232h, conn_open)
|
self._set_conn_indicator(self._conn_ft2232h, conn_open)
|
||||||
self._set_conn_indicator(self._conn_stm32, self._stm32.is_open)
|
self._set_conn_indicator(self._conn_stm32, self._stm32.is_open)
|
||||||
|
|
||||||
|
# Update USB label to reflect which interface is active
|
||||||
|
if isinstance(self._connection, FT601Connection):
|
||||||
|
self._conn_usb_label.setText("FT601:")
|
||||||
|
else:
|
||||||
|
self._conn_usb_label.setText("FT2232H:")
|
||||||
|
|
||||||
gps_count = self._gps_packet_count
|
gps_count = self._gps_packet_count
|
||||||
if self._gps_worker:
|
if self._gps_worker:
|
||||||
gps_count = self._gps_worker.gps_count
|
gps_count = self._gps_worker.gps_count
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ if USB_AVAILABLE:
|
|||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||||
from radar_protocol import ( # noqa: F401 — re-exported for v7 package
|
from radar_protocol import ( # noqa: F401 — re-exported for v7 package
|
||||||
FT2232HConnection,
|
FT2232HConnection,
|
||||||
|
FT601Connection,
|
||||||
RadarProtocol,
|
RadarProtocol,
|
||||||
Opcode,
|
Opcode,
|
||||||
RadarAcquisition,
|
RadarAcquisition,
|
||||||
@@ -46,8 +47,9 @@ class STM32USBInterface:
|
|||||||
|
|
||||||
Used ONLY for receiving GPS data from the MCU.
|
Used ONLY for receiving GPS data from the MCU.
|
||||||
|
|
||||||
FPGA register commands are sent via FT2232H (see FT2232HConnection
|
FPGA register commands are sent via the USB data interface — either
|
||||||
from radar_protocol.py). The old send_start_flag() / send_settings()
|
FT2232HConnection (production) or FT601Connection (premium), both
|
||||||
|
from radar_protocol.py. The old send_start_flag() / send_settings()
|
||||||
methods have been removed — they used an incompatible magic-packet
|
methods have been removed — they used an incompatible magic-packet
|
||||||
protocol that the FPGA does not understand.
|
protocol that the FPGA does not understand.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ class RadarMapWidget(QWidget):
|
|||||||
)
|
)
|
||||||
self._targets: list[RadarTarget] = []
|
self._targets: list[RadarTarget] = []
|
||||||
self._pending_targets: list[RadarTarget] | None = None
|
self._pending_targets: list[RadarTarget] | None = None
|
||||||
self._coverage_radius = 50_000 # metres
|
self._coverage_radius = 1_536 # metres (64 bins x ~24 m/bin)
|
||||||
self._tile_server = TileServer.OPENSTREETMAP
|
self._tile_server = TileServer.OPENSTREETMAP
|
||||||
self._show_coverage = True
|
self._show_coverage = True
|
||||||
self._show_trails = False
|
self._show_trails = False
|
||||||
|
|||||||
@@ -108,12 +108,12 @@ class RadarSettings:
|
|||||||
range_resolution and velocity_resolution should be calibrated to
|
range_resolution and velocity_resolution should be calibrated to
|
||||||
the actual waveform parameters.
|
the actual waveform parameters.
|
||||||
"""
|
"""
|
||||||
system_frequency: float = 10e9 # Hz (carrier, used for velocity calc)
|
system_frequency: float = 10.5e9 # Hz (carrier, used for velocity calc)
|
||||||
range_resolution: float = 781.25 # Meters per range bin (default: 50km/64)
|
range_resolution: float = 24.0 # Meters per range bin (c/(2*Fs)*decim)
|
||||||
velocity_resolution: float = 1.0 # m/s per Doppler bin (calibrate to waveform)
|
velocity_resolution: float = 1.0 # m/s per Doppler bin (calibrate to waveform)
|
||||||
max_distance: float = 50000 # Max detection range (m)
|
max_distance: float = 1536 # Max detection range (m)
|
||||||
map_size: float = 50000 # Map display size (m)
|
map_size: float = 2000 # Map display size (m)
|
||||||
coverage_radius: float = 50000 # Map coverage radius (m)
|
coverage_radius: float = 1536 # Map coverage radius (m)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -199,39 +199,46 @@ class WaveformConfig:
|
|||||||
Encapsulates the radar waveform so that range/velocity resolution
|
Encapsulates the radar waveform so that range/velocity resolution
|
||||||
can be derived automatically instead of hardcoded in RadarSettings.
|
can be derived automatically instead of hardcoded in RadarSettings.
|
||||||
|
|
||||||
Defaults match the ADI CN0566 Phaser capture parameters used in
|
Defaults match the AERIS-10 production system parameters from
|
||||||
the golden_reference cosim (4 MSPS, 500 MHz BW, 300 us chirp).
|
radar_scene.py / plfm_chirp_controller.v:
|
||||||
|
100 MSPS DDC output, 20 MHz chirp BW, 30 us long chirp,
|
||||||
|
167 us long-chirp PRI, X-band 10.5 GHz carrier.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
sample_rate_hz: float = 4e6 # ADC sample rate
|
sample_rate_hz: float = 100e6 # DDC output I/Q rate (matched filter input)
|
||||||
bandwidth_hz: float = 500e6 # Chirp bandwidth
|
bandwidth_hz: float = 20e6 # Chirp bandwidth (not used in range calc;
|
||||||
chirp_duration_s: float = 300e-6 # Chirp ramp time
|
# retained for time-bandwidth product / display)
|
||||||
center_freq_hz: float = 10.525e9 # Carrier frequency
|
chirp_duration_s: float = 30e-6 # Long chirp ramp time
|
||||||
|
pri_s: float = 167e-6 # Pulse repetition interval (chirp + listen)
|
||||||
|
center_freq_hz: float = 10.5e9 # Carrier frequency (radar_scene.py: F_CARRIER)
|
||||||
n_range_bins: int = 64 # After decimation
|
n_range_bins: int = 64 # After decimation
|
||||||
n_doppler_bins: int = 32 # After Doppler FFT
|
n_doppler_bins: int = 32 # Total Doppler bins (2 sub-frames x 16)
|
||||||
|
chirps_per_subframe: int = 16 # Chirps in one Doppler sub-frame
|
||||||
fft_size: int = 1024 # Pre-decimation FFT length
|
fft_size: int = 1024 # Pre-decimation FFT length
|
||||||
decimation_factor: int = 16 # 1024 → 64
|
decimation_factor: int = 16 # 1024 → 64
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def range_resolution_m(self) -> float:
|
def range_resolution_m(self) -> float:
|
||||||
"""Meters per decimated range bin (FMCW deramped baseband).
|
"""Meters per decimated range bin (matched-filter pulse compression).
|
||||||
|
|
||||||
For deramped FMCW: bin spacing = c * Fs * T / (2 * N_FFT * BW).
|
For FFT-based matched filtering, each IFFT output bin spans
|
||||||
After decimation the bin spacing grows by *decimation_factor*.
|
c / (2 * Fs) in range, where Fs is the I/Q sample rate at the
|
||||||
|
matched-filter input (DDC output). After decimation the bin
|
||||||
|
spacing grows by *decimation_factor*.
|
||||||
"""
|
"""
|
||||||
c = 299_792_458.0
|
c = 299_792_458.0
|
||||||
raw_bin = (
|
raw_bin = c / (2.0 * self.sample_rate_hz)
|
||||||
c * self.sample_rate_hz * self.chirp_duration_s
|
|
||||||
/ (2.0 * self.fft_size * self.bandwidth_hz)
|
|
||||||
)
|
|
||||||
return raw_bin * self.decimation_factor
|
return raw_bin * self.decimation_factor
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def velocity_resolution_mps(self) -> float:
|
def velocity_resolution_mps(self) -> float:
|
||||||
"""m/s per Doppler bin. lambda / (2 * n_doppler * chirp_duration)."""
|
"""m/s per Doppler bin.
|
||||||
|
|
||||||
|
lambda / (2 * chirps_per_subframe * PRI), matching radar_scene.py.
|
||||||
|
"""
|
||||||
c = 299_792_458.0
|
c = 299_792_458.0
|
||||||
wavelength = c / self.center_freq_hz
|
wavelength = c / self.center_freq_hz
|
||||||
return wavelength / (2.0 * self.n_doppler_bins * self.chirp_duration_s)
|
return wavelength / (2.0 * self.chirps_per_subframe * self.pri_s)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def max_range_m(self) -> float:
|
def max_range_m(self) -> float:
|
||||||
|
|||||||
@@ -334,7 +334,7 @@ class TargetSimulator(QObject):
|
|||||||
self._add_random_target()
|
self._add_random_target()
|
||||||
|
|
||||||
def _add_random_target(self):
|
def _add_random_target(self):
|
||||||
range_m = random.uniform(5000, 40000)
|
range_m = random.uniform(50, 1400)
|
||||||
azimuth = random.uniform(0, 360)
|
azimuth = random.uniform(0, 360)
|
||||||
velocity = random.uniform(-100, 100)
|
velocity = random.uniform(-100, 100)
|
||||||
elevation = random.uniform(-5, 45)
|
elevation = random.uniform(-5, 45)
|
||||||
@@ -368,7 +368,7 @@ class TargetSimulator(QObject):
|
|||||||
|
|
||||||
for t in self._targets:
|
for t in self._targets:
|
||||||
new_range = t.range - t.velocity * 0.5
|
new_range = t.range - t.velocity * 0.5
|
||||||
if new_range < 500 or new_range > 50000:
|
if new_range < 10 or new_range > 1536:
|
||||||
continue # target exits coverage — drop it
|
continue # target exits coverage — drop it
|
||||||
|
|
||||||
new_vel = max(-150, min(150, t.velocity + random.uniform(-2, 2)))
|
new_vel = max(-150, min(150, t.velocity + random.uniform(-2, 2)))
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ def parse_python_data_packet_fields(filepath: Path | None = None) -> list[DataPa
|
|||||||
width_bits=size * 8
|
width_bits=size * 8
|
||||||
))
|
))
|
||||||
|
|
||||||
# Match detection = raw[9] & 0x01
|
# Match detection = raw[9] & 0x01 (direct access)
|
||||||
for m in re.finditer(r'(\w+)\s*=\s*raw\[(\d+)\]\s*&\s*(0x[0-9a-fA-F]+|\d+)', body):
|
for m in re.finditer(r'(\w+)\s*=\s*raw\[(\d+)\]\s*&\s*(0x[0-9a-fA-F]+|\d+)', body):
|
||||||
name = m.group(1)
|
name = m.group(1)
|
||||||
offset = int(m.group(2))
|
offset = int(m.group(2))
|
||||||
@@ -196,6 +196,24 @@ def parse_python_data_packet_fields(filepath: Path | None = None) -> list[DataPa
|
|||||||
name=name, byte_start=offset, byte_end=offset, width_bits=1
|
name=name, byte_start=offset, byte_end=offset, width_bits=1
|
||||||
))
|
))
|
||||||
|
|
||||||
|
# Match intermediate variable pattern: var = raw[N], then field = var & MASK
|
||||||
|
for m in re.finditer(r'(\w+)\s*=\s*raw\[(\d+)\]', body):
|
||||||
|
var_name = m.group(1)
|
||||||
|
offset = int(m.group(2))
|
||||||
|
# Find fields derived from this intermediate variable
|
||||||
|
for m2 in re.finditer(
|
||||||
|
rf'(\w+)\s*=\s*(?:\({var_name}\s*>>\s*\d+\)\s*&|{var_name}\s*&)\s*'
|
||||||
|
r'(0x[0-9a-fA-F]+|\d+)',
|
||||||
|
body,
|
||||||
|
):
|
||||||
|
name = m2.group(1)
|
||||||
|
# Skip if already captured by direct raw[] access pattern
|
||||||
|
if not any(f.name == name for f in fields):
|
||||||
|
fields.append(DataPacketField(
|
||||||
|
name=name, byte_start=offset, byte_end=offset,
|
||||||
|
width_bits=1
|
||||||
|
))
|
||||||
|
|
||||||
fields.sort(key=lambda f: f.byte_start)
|
fields.sort(key=lambda f: f.byte_start)
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
@@ -497,7 +515,6 @@ 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,
|
||||||
@@ -584,12 +601,28 @@ def parse_verilog_data_mux(
|
|||||||
|
|
||||||
for m in re.finditer(
|
for m in re.finditer(
|
||||||
r"5'd(\d+)\s*:\s*data_pkt_byte\s*=\s*(.+?);",
|
r"5'd(\d+)\s*:\s*data_pkt_byte\s*=\s*(.+?);",
|
||||||
mux_body
|
mux_body, re.DOTALL
|
||||||
):
|
):
|
||||||
idx = int(m.group(1))
|
idx = int(m.group(1))
|
||||||
expr = m.group(2).strip()
|
expr = m.group(2).strip()
|
||||||
entries.append((idx, expr))
|
entries.append((idx, expr))
|
||||||
|
|
||||||
|
# Helper: extract the dominant signal name from a mux expression.
|
||||||
|
# Handles direct refs like ``range_profile_cap[31:24]``, ternaries
|
||||||
|
# like ``stream_doppler_en ? doppler_real_cap[15:8] : 8'd0``, and
|
||||||
|
# concat-ternaries like ``stream_cfar_en ? {…, cfar_detection_cap} : …``.
|
||||||
|
def _extract_signal(expr: str) -> str | None:
|
||||||
|
# If it's a ternary, use the true-branch to find the data signal
|
||||||
|
tern = re.match(r'\w+\s*\?\s*(.+?)\s*:\s*.+', expr, re.DOTALL)
|
||||||
|
target = tern.group(1) if tern else expr
|
||||||
|
# Look for a known data signal (xxx_cap pattern or cfar_detection_cap)
|
||||||
|
cap_match = re.search(r'(\w+_cap)\b', target)
|
||||||
|
if cap_match:
|
||||||
|
return cap_match.group(1)
|
||||||
|
# Fall back to first identifier before a bit-select
|
||||||
|
sig_match = re.match(r'(\w+?)(?:\[|$)', target)
|
||||||
|
return sig_match.group(1) if sig_match else None
|
||||||
|
|
||||||
# Group consecutive bytes by signal root name
|
# Group consecutive bytes by signal root name
|
||||||
fields: list[DataPacketField] = []
|
fields: list[DataPacketField] = []
|
||||||
i = 0
|
i = 0
|
||||||
@@ -599,22 +632,21 @@ def parse_verilog_data_mux(
|
|||||||
i += 1
|
i += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Extract signal name (e.g., range_profile_cap from range_profile_cap[31:24])
|
signal = _extract_signal(expr)
|
||||||
sig_match = re.match(r'(\w+?)(?:\[|$)', expr)
|
if not signal:
|
||||||
if not sig_match:
|
|
||||||
i += 1
|
i += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
signal = sig_match.group(1)
|
|
||||||
start_byte = idx
|
start_byte = idx
|
||||||
end_byte = idx
|
end_byte = idx
|
||||||
|
|
||||||
# Find consecutive bytes of the same signal
|
# Find consecutive bytes of the same signal
|
||||||
j = i + 1
|
j = i + 1
|
||||||
while j < len(entries):
|
while j < len(entries):
|
||||||
next_idx, next_expr = entries[j]
|
_next_idx, next_expr = entries[j]
|
||||||
if next_expr.startswith(signal):
|
next_sig = _extract_signal(next_expr)
|
||||||
end_byte = next_idx
|
if next_sig == signal:
|
||||||
|
end_byte = _next_idx
|
||||||
j += 1
|
j += 1
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -620,8 +620,10 @@ module tb_cross_layer_ft2232h;
|
|||||||
"Data pkt: byte 7 = 0x56 (doppler_imag MSB)");
|
"Data pkt: byte 7 = 0x56 (doppler_imag MSB)");
|
||||||
check(captured_bytes[8] === 8'h78,
|
check(captured_bytes[8] === 8'h78,
|
||||||
"Data pkt: byte 8 = 0x78 (doppler_imag LSB)");
|
"Data pkt: byte 8 = 0x78 (doppler_imag LSB)");
|
||||||
check(captured_bytes[9] === 8'h01,
|
// Byte 9 = {frame_start, 6'b0, cfar_detection}
|
||||||
"Data pkt: byte 9 = 0x01 (cfar_detection=1)");
|
// After reset sample_counter==0, so frame_start=1 → 0x81
|
||||||
|
check(captured_bytes[9] === 8'h81,
|
||||||
|
"Data pkt: byte 9 = 0x81 (frame_start=1, cfar_detection=1)");
|
||||||
check(captured_bytes[10] === 8'h55,
|
check(captured_bytes[10] === 8'h55,
|
||||||
"Data pkt: byte 10 = 0x55 (footer)");
|
"Data pkt: byte 10 = 0x55 (footer)");
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ layers agree (because both could be wrong).
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import struct
|
import struct
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -50,8 +49,8 @@ sys.path.insert(0, str(cp.GUI_DIR))
|
|||||||
# Helpers
|
# Helpers
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
|
||||||
IVERILOG = os.environ.get("IVERILOG", "iverilog")
|
IVERILOG = os.environ.get("IVERILOG", "/opt/homebrew/bin/iverilog")
|
||||||
VVP = os.environ.get("VVP", "vvp")
|
VVP = os.environ.get("VVP", "/opt/homebrew/bin/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
|
||||||
@@ -62,20 +61,6 @@ _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 _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."""
|
||||||
@@ -370,188 +355,6 @@ 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.
|
|
||||||
"""
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestTier1DataPacketLayout:
|
class TestTier1DataPacketLayout:
|
||||||
"""Verify data packet byte layout matches between Python and Verilog."""
|
"""Verify data packet byte layout matches between Python and Verilog."""
|
||||||
|
|
||||||
|
|||||||
@@ -111,7 +111,8 @@ The AERIS-10 main sub-systems are:
|
|||||||
- Map integration
|
- Map integration
|
||||||
- Radar control interface
|
- Radar control interface
|
||||||
|
|
||||||

|

|
||||||
|
<!-- V6 GIF removed — V6 is deprecated. V65 Tk and V7 PyQt6 are the active GUIs. -->
|
||||||
|
|
||||||
## 📊 Technical Specifications
|
## 📊 Technical Specifications
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,11 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="stats-grid">
|
<section class="stats-grid">
|
||||||
|
<article class="card stat notice">
|
||||||
|
<h2>Production Board USB</h2>
|
||||||
|
<p class="metric">FT2232H (USB 2.0)</p>
|
||||||
|
<p class="muted">50T production board uses FT2232H. FT601 USB 3.0 is available on 200T premium dev board only.</p>
|
||||||
|
</article>
|
||||||
<article class="card stat">
|
<article class="card stat">
|
||||||
<h2>Tracked Timing Baseline</h2>
|
<h2>Tracked Timing Baseline</h2>
|
||||||
<p class="metric">WNS +0.058 ns</p>
|
<p class="metric">WNS +0.058 ns</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user