Volvo EX90 雷达 CPD 技术解析:60GHz 三雷达方案详解

车型: Volvo EX90 (2024)
技术: 60GHz 毫米波雷达 CPD(儿童存在检测)
配置: 3 个雷达传感器,全覆盖车舱
Euro NCAP 合规: ✅ 2026 CPD 要求


技术背景

CPD 法规要求

Euro NCAP 2026 CPD 要求:

项目 要求 说明
检测对象 儿童/婴儿 ≤6岁儿童
检测场景 锁车后车内 静止/睡眠状态
检测时限 ≤60秒 锁车后开始计时
警告方式 声光+手机通知 多级警告
误报率 <1次/周 正常使用不骚扰
漏报率 0% 儿童不得遗漏

为什么选择雷达?

技术 优势 劣势 CPD 适用性
60GHz 雷达 穿透性好、隐私保护、全光照 成本中 ✅ 高
摄像头 信息丰富、可分类 隐私问题、遮挡敏感 ⚠️ 中
超声波 成本低 受温度影响、分辨率低 ❌ 低
压力传感器 成本低、可靠 仅检测重量、无呼吸 ⚠️ 中

Volvo 选择雷达的核心原因:

  1. 可检测呼吸/心跳等生命体征
  2. 穿透毛毯/衣物,不受遮挡影响
  3. 全天候工作,不受光照影响
  4. 隐私友好,无图像采集

系统架构

三雷达配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
┌─────────────────────────────────────────────────┐
│ Volvo EX90 CPD 系统 │
├─────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────┐ │
│ │ 雷达1(顶棚) │ │
│ │ 覆盖前排座椅区域 │ │
│ └───────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────┐ │
│ │ │ │
│ │ ┌──────┐ ┌──────┐ │ │
│ │ │ 驾驶 │ │ 乘客 │ │ │
│ │ │ 座椅 │ │ 座椅 │ │ │
│ │ └──────┘ └──────┘ │ │
│ │ │ │
│ │ ┌──────┐ ┌──────┐ │ │
│ │ │ 后排 │ │ 后排 │ │ │
│ │ │ 左 │ │ 右 │ │ │
│ │ └──────┘ └──────┘ │ │
│ │ │ │
│ └──────────────────────────────────────┘ │
│ ↑ ↑ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 雷达2(左侧)│ │ 雷达3(右侧)│ │
│ │ 覆盖后排左 │ │ 覆盖后排右 │ │
│ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────┘

三个雷达的分工:

雷达 安装位置 覆盖区域 主要功能
雷达1 车顶控制台 前排座椅 驾驶员/乘客检测
雷达2 左侧 B 柱 后排左侧 儿童座椅位置检测
雷达3 右侧 C 柱 后排右侧 儿童座椅位置检测

硬件规格

参数 规格
频率 60-64 GHz
带宽 4 GHz
扫描范围 ±60° 水平,±30° 垂直
距离分辨率 ~5 cm
速度分辨率 ~0.2 m/s
更新频率 10 Hz
功耗 <5W/雷达
尺寸 70×50×15 mm

技术实现

1. FMCW 雷达原理

Frequency Modulated Continuous Wave (FMCW) 雷达:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
发射信号频率:
f_tx(t) = f_0 + K × t

其中:
- f_0: 起始频率 (60 GHz)
- K: 调频斜率 (K = B / T_c)
- B: 带宽 (4 GHz)
- T_c: 扫描周期 (100 μs)

接收信号频率(经过时间延迟 τ):
f_rx(t) = f_0 + K × (t - τ)

差频信号:
f_beat = f_tx - f_rx = K × τ

距离计算:
R = c × τ / 2 = c × f_beat / (2 × K)

代码实现:

import numpy as np
from typing import List, Tuple
from dataclasses import dataclass

@dataclass
class RadarConfig:
    """雷达配置参数"""
    # 频率参数
    f0: float = 60e9  # 起始频率 (Hz)
    bandwidth: float = 4e9  # 带宽 (Hz)
    
    # 时序参数
    chirp_duration: float = 100e-6  # 扫描周期 (s)
    num_chirps: int = 128  # 每帧 chirp 数
    
    # 天线参数
    num_tx: int = 3  # 发射天线
    num_rx: int = 4  # 接收天线
    
    # 采样参数
    adc_rate: float = 10e6  # ADC 采样率 (Hz)
    num_samples: int = 256  # 每 chirp 采样点


class FMCWRadar:
    """
    60GHz FMCW 雷达模拟器
    
    用于 CPD 检测
    """
    
    def __init__(self, config: RadarConfig):
        self.config = config
        
        # 计算雷达参数
        self.range_resolution = self._calculate_range_resolution()
        self.velocity_resolution = self._calculate_velocity_resolution()
        self.max_range = self._calculate_max_range()
        self.max_velocity = self._calculate_max_velocity()
    
    def _calculate_range_resolution(self) -> float:
        """
        计算距离分辨率
        
        ΔR = c / (2 × B)
        """
        c = 3e8  # 光速
        return c / (2 * self.config.bandwidth)
    
    def _calculate_velocity_resolution(self) -> float:
        """
        计算速度分辨率
        
        Δv = λ / (2 × N_chirps × T_c)
        """
        c = 3e8
        wavelength = c / self.config.f0
        return wavelength / (
            2 * self.config.num_chirps * self.config.chirp_duration
        )
    
    def _calculate_max_range(self) -> float:
        """
        计算最大探测距离
        
        R_max = (f_sample_max × c) / (2 × K)
        """
        c = 3e8
        K = self.config.bandwidth / self.config.chirp_duration
        f_beat_max = self.config.adc_rate / 2
        
        return (f_beat_max * c) / (2 * K)
    
    def _calculate_max_velocity(self) -> float:
        """
        计算最大可测速度
        
        v_max = λ / (4 × T_c)
        """
        c = 3e8
        wavelength = c / self.config.f0
        return wavelength / (4 * self.config.chirp_duration)
    
    def generate_chirp_signal(
        self,
        target_distance: float,
        target_velocity: float
    ) -> np.ndarray:
        """
        生成 chirp 信号
        
        Args:
            target_distance: 目标距离 (m)
            target_velocity: 目标速度 (m/s)
            
        Returns:
            beat_signal: 差频信号
        """
        c = 3e8
        wavelength = c / self.config.f0
        K = self.config.bandwidth / self.config.chirp_duration
        
        # 时间延迟
        tau = 2 * target_distance / c
        
        # 多普勒频移
        f_doppler = 2 * target_velocity / wavelength
        
        # 采样时间
        t = np.arange(self.config.num_samples) / self.config.adc_rate
        
        # 差频信号
        # 距离项 + 多普勒项
        f_beat = K * tau + f_doppler
        phase = 2 * np.pi * f_beat * t
        
        beat_signal = np.exp(1j * phase)
        
        return beat_signal
    
    def generate_frame(
        self,
        targets: List[Tuple[float, float]]
    ) -> np.ndarray:
        """
        生成一帧数据(多个 chirp)
        
        Args:
            targets: [(distance, velocity), ...]
            
        Returns:
            frame: (num_chirps, num_samples)
        """
        frame = np.zeros(
            (self.config.num_chirps, self.config.num_samples),
            dtype=complex
        )
        
        for chirp_idx in range(self.config.num_chirps):
            chirp_signal = np.zeros(self.config.num_samples, dtype=complex)
            
            for distance, velocity in targets:
                # 每个目标的相位随 chirp 变化(多普勒)
                signal = self.generate_chirp_signal(distance, velocity)
                chirp_signal += signal
            
            frame[chirp_idx] = chirp_signal
        
        return frame
    
    def range_fft(self, frame: np.ndarray) -> np.ndarray:
        """
        距离 FFT
        
        Args:
            frame: (num_chirps, num_samples)
            
        Returns:
            range_profile: (num_chirps, num_range_bins)
        """
        # 对每个 chirp 做 FFT
        range_fft = np.fft.fft(frame, axis=1)
        
        # 转换为距离
        range_bins = np.fft.fftfreq(
            self.config.num_samples,
            1 / self.config.adc_rate
        )
        
        # 差频 → 距离
        c = 3e8
        K = self.config.bandwidth / self.config.chirp_duration
        distances = (range_bins * c) / (2 * K)
        
        return range_fft, distances
    
    def doppler_fft(self, range_fft: np.ndarray) -> np.ndarray:
        """
        多普勒 FFT
        
        Args:
            range_fft: (num_chirps, num_range_bins)
            
        Returns:
            range_doppler_map: (num_doppler_bins, num_range_bins)
        """
        # 对每个距离 bin 做 FFT
        rd_map = np.fft.fftshift(
            np.fft.fft(range_fft, axis=0),
            axes=0
        )
        
        return rd_map
    
    def detect_targets(
        self,
        rd_map: np.ndarray,
        threshold_db: float = -30
    ) -> List[Tuple[float, float, float]]:
        """
        目标检测(CFAR)
        
        Args:
            rd_map: Range-Doppler map
            threshold_db: 检测阈值 (dB)
            
        Returns:
            targets: [(distance, velocity, power), ...]
        """
        # 转换为 dB
        rd_map_db = 20 * np.log10(np.abs(rd_map) + 1e-10)
        
        # 阈值检测
        detections = np.where(rd_map_db > threshold_db)
        
        targets = []
        c = 3e8
        wavelength = c / self.config.f0
        
        for doppler_idx, range_idx in zip(*detections):
            # 距离
            distance = range_idx * self.range_resolution
            
            # 速度
            doppler_bin = doppler_idx - self.config.num_chirps // 2
            velocity = doppler_bin * self.velocity_resolution
            
            # 功率
            power = rd_map_db[doppler_idx, range_idx]
            
            targets.append((distance, velocity, power))
        
        return targets


class CPDDetector:
    """
    儿童存在检测器
    
    基于 60GHz 雷达
    """
    
    # 生命体征参数
    BREATHING_RATE_RANGE = (0.2, 1.0)  # Hz (12-60 次/分)
    HEART_RATE_RANGE = (1.0, 2.0)  # Hz (60-120 次/分)
    BREATHING_AMPLITUDE = (0.5, 5.0)  # mm (胸部运动)
    
    def __init__(self):
        self.radar = FMCWRadar(RadarConfig())
        
        # 状态
        self.detection_history = []
        self.history_duration = 60  # 60秒历史
    
    def detect_vital_signs(
        self,
        rd_map: np.ndarray,
        distance_range: Tuple[float, float] = (0.3, 2.0)
    ) -> dict:
        """
        检测生命体征
        
        Args:
            rd_map: Range-Doppler map
            distance_range: 感兴趣的距离范围 (m)
            
        Returns:
            vital_signs: {
                "breathing_detected": bool,
                "breathing_rate": Hz,
                "movement_detected": bool,
                "confidence": float
            }
        """
        # 提取低速区域(呼吸/心跳)
        # 速度范围:±0.05 m/s
        velocity_limit = 0.05
        doppler_bins = int(velocity_limit / self.radar.velocity_resolution)
        
        # 提取感兴趣区域
        rd_roi = rd_map[
            self.radar.config.num_chirps // 2 - doppler_bins:
            self.radar.config.num_chirps // 2 + doppler_bins,
            :
        ]
        
        # 时域分析(沿 chirp 维度)
        # 检测周期性运动
        time_signal = np.sum(np.abs(rd_roi), axis=1)
        
        # FFT 分析呼吸频率
        freq_spectrum = np.abs(np.fft.fft(time_signal))
        freqs = np.fft.fftfreq(len(time_signal), self.radar.chirp_duration)
        
        # 找呼吸峰
        breathing_mask = (
            (np.abs(freqs) >= self.BREATHING_RATE_RANGE[0]) &
            (np.abs(freqs) <= self.BREATHING_RATE_RANGE[1])
        )
        
        breathing_spectrum = freq_spectrum.copy()
        breathing_spectrum[~breathing_mask] = 0
        
        breathing_peak_idx = np.argmax(breathing_spectrum)
        breathing_rate = np.abs(freqs[breathing_peak_idx])
        
        # 判断是否检测到呼吸
        breathing_detected = (
            breathing_rate > 0 and
            breathing_spectrum[breathing_peak_idx] > np.mean(freq_spectrum) * 2
        )
        
        # 检测大幅度运动
        movement_threshold = 0.5  # m/s
        movement_mask = np.abs(rd_map) > np.max(np.abs(rd_map)) * 0.5
        
        # 排除零速度区域
        zero_velocity_region = slice(
            self.radar.config.num_chirps // 2 - 2,
            self.radar.config.num_chirps // 2 + 2
        )
        movement_mask[zero_velocity_region, :] = False
        
        movement_detected = np.any(movement_mask)
        
        # 置信度
        confidence = min(1.0, breathing_spectrum[breathing_peak_idx] / 
                        (np.mean(freq_spectrum) * 5))
        
        return {
            "breathing_detected": breathing_detected,
            "breathing_rate": breathing_rate,
            "movement_detected": movement_detected,
            "confidence": confidence
        }
    
    def classify_target(
        self,
        vital_signs: dict,
        target_info: dict
    ) -> str:
        """
        分类目标类型
        
        Args:
            vital_signs: 生命体征检测结果
            target_info: 目标位置信息
            
        Returns:
            classification: "child" / "pet" / "adult" / "unknown"
        """
        # 儿童检测逻辑
        # 1. 呼吸存在
        # 2. 目标在儿童座椅位置(后排)
        # 3. 无大幅度运动
        
        if vital_signs["breathing_detected"]:
            if target_info.get("location") == "rear_seat":
                # 检查运动幅度
                if not vital_signs["movement_detected"]:
                    return "child"
                else:
                    return "pet"  # 可能是宠物
            
            elif target_info.get("location") == "front_seat":
                return "adult"
        
        return "unknown"
    
    def monitor(
        self,
        frames: List[np.ndarray],
        duration: float
    ) -> dict:
        """
        持续监控
        
        Args:
            frames: 连续帧数据
            duration: 监控时长 (秒)
            
        Returns:
            result: {
                "child_detected": bool,
                "location": str,
                "confidence": float,
                "vital_signs": dict
            }
        """
        # 处理所有帧
        vital_signs_list = []
        
        for frame in frames:
            rd_map, distances = self.radar.range_fft(frame)
            rd_map = self.radar.doppler_fft(rd_map)
            
            vital_signs = self.detect_vital_signs(rd_map)
            vital_signs_list.append(vital_signs)
        
        # 统计
        breathing_count = sum(
            1 for vs in vital_signs_list if vs["breathing_detected"]
        )
        breathing_ratio = breathing_count / len(frames)
        
        # 最终判定
        child_detected = breathing_ratio > 0.7  # 70% 以上帧检测到呼吸
        
        # 平均呼吸率
        valid_rates = [
            vs["breathing_rate"] 
            for vs in vital_signs_list 
            if vs["breathing_detected"]
        ]
        avg_breathing_rate = np.mean(valid_rates) if valid_rates else 0
        
        return {
            "child_detected": child_detected,
            "location": "rear_seat",  # 简化
            "confidence": breathing_ratio,
            "breathing_rate": avg_breathing_rate,
            "vital_signs_history": vital_signs_list
        }


# 测试
if __name__ == "__main__":
    # 初始化
    config = RadarConfig()
    radar = FMCWRadar(config)
    cpd = CPDDetector()
    
    print("=== 60GHz 雷达 CPD 系统参数 ===")
    print(f"距离分辨率: {radar.range_resolution * 100:.1f} cm")
    print(f"速度分辨率: {radar.velocity_resolution:.3f} m/s")
    print(f"最大距离: {radar.max_range:.1f} m")
    print(f"最大速度: {radar.max_velocity:.1f} m/s")
    
    # 模拟目标
    targets = [
        (1.0, 0.01),  # 1米处,呼吸运动 (1 cm/s)
        (1.0, 0.3),   # 同距离,大幅度运动
    ]
    
    # 生成信号
    frame = radar.generate_frame(targets)
    
    # 处理
    rd_map, distances = radar.range_fft(frame)
    rd_map = radar.doppler_fft(rd_map)
    
    # 检测生命体征
    vital_signs = cpd.detect_vital_signs(rd_map)
    
    print("\n=== 生命体征检测 ===")
    print(f"呼吸检测: {'✅' if vital_signs['breathing_detected'] else '❌'}")
    print(f"呼吸频率: {vital_signs['breathing_rate'] * 60:.1f} 次/分")
    print(f"运动检测: {'✅' if vital_signs['movement_detected'] else '❌'}")
    print(f"置信度: {vital_signs['confidence']:.2f}")

Volvo EX90 雷达 CPD 技术解析:60GHz 三雷达方案详解
https://dapalm.com/2026/04/21/2026-04-21-volvo-ex90-cpd-radar/
作者
Mars
发布于
2026年4月21日
许可协议