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 | |
三个雷达的分工:
| 雷达 | 安装位置 | 覆盖区域 | 主要功能 |
|---|---|---|---|
| 雷达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 | |
代码实现:
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/