IQ.Pilot Release Commit @ 0798119
This commit is contained in:
@@ -29,9 +29,26 @@ class CarController(CarControllerBase):
|
|||||||
# 0x1E2/0x316 go out unconditionally, gated only by STEER_REQ: the safety blocks the
|
# 0x1E2/0x316 go out unconditionally, gated only by STEER_REQ: the safety blocks the
|
||||||
# camera's copies, and the EPS latches a fault if the stream stops while it is actuating.
|
# camera's copies, and the EPS latches a fault if the stream stops while it is actuating.
|
||||||
if self.frame % CarControllerParams.STEER_STEP == 0:
|
if self.frame % CarControllerParams.STEER_STEP == 0:
|
||||||
self.apply_angle_last = apply_steer_angle_limits_vm(actuators.steeringAngleDeg, self.apply_angle_last,
|
apply_angle = apply_steer_angle_limits_vm(actuators.steeringAngleDeg, self.apply_angle_last,
|
||||||
CS.out.vEgoRaw, CS.out.steeringAngleDeg,
|
CS.out.vEgoRaw, CS.out.steeringAngleDeg,
|
||||||
CC.latActive, CarControllerParams, self.VM)
|
CC.latActive, CarControllerParams, self.VM)
|
||||||
|
|
||||||
|
# The vehicle-model jerk limit stops binding below a few m/s, so cap the slew rate
|
||||||
|
# directly there. Without this the planner's standstill oscillation drives the command
|
||||||
|
# tens of degrees away from a stationary wheel and the EPS latches state 11.
|
||||||
|
if CC.latActive:
|
||||||
|
max_rate = float(np.interp(CS.out.vEgoRaw, CarControllerParams.ANGLE_RATE_BP,
|
||||||
|
CarControllerParams.ANGLE_RATE_V))
|
||||||
|
apply_angle = float(np.clip(apply_angle, self.apply_angle_last - max_rate,
|
||||||
|
self.apply_angle_last + max_rate))
|
||||||
|
|
||||||
|
# Never wind the command away from the wheel: the EPS latches on angle divergence, and
|
||||||
|
# a driver holding the wheel below the override threshold would otherwise let the
|
||||||
|
# controller run tens of degrees past it.
|
||||||
|
err = CarControllerParams.MAX_ANGLE_ERROR
|
||||||
|
apply_angle = float(np.clip(apply_angle, CS.out.steeringAngleDeg - err,
|
||||||
|
CS.out.steeringAngleDeg + err))
|
||||||
|
self.apply_angle_last = apply_angle
|
||||||
|
|
||||||
can_sends.append(bydcan.create_steering_control(self.packer, self.apply_angle_last, CC.latActive))
|
can_sends.append(bydcan.create_steering_control(self.packer, self.apply_angle_last, CC.latActive))
|
||||||
can_sends.append(bydcan.create_lkas_hud(self.packer, CC.latActive, CS.lkas_hud, CC.hudControl))
|
can_sends.append(bydcan.create_lkas_hud(self.packer, CC.latActive, CS.lkas_hud, CC.hudControl))
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ EPS_STATE_PREPARED = 9
|
|||||||
EPS_STATE_ACTUATING = 10
|
EPS_STATE_ACTUATING = 10
|
||||||
EPS_STATE_LATCHED_FAULT = 11
|
EPS_STATE_LATCHED_FAULT = 11
|
||||||
|
|
||||||
|
# ACC_HUD_ADAS.CRUISE_STATE
|
||||||
|
CRUISE_STATE_AVAILABLE = 1
|
||||||
|
CRUISE_STATE_ENGAGED = 2
|
||||||
|
|
||||||
|
|
||||||
class CarState(CarStateBase):
|
class CarState(CarStateBase):
|
||||||
def __init__(self, CP, CP_IQ):
|
def __init__(self, CP, CP_IQ):
|
||||||
@@ -29,6 +33,8 @@ class CarState(CarStateBase):
|
|||||||
self.acc_cmd = {}
|
self.acc_cmd = {}
|
||||||
self.buttons = {}
|
self.buttons = {}
|
||||||
self.eps_state = EPS_STATE_OFF
|
self.eps_state = EPS_STATE_OFF
|
||||||
|
self.override_latched = False
|
||||||
|
self.lkas_btn_prev = False
|
||||||
|
|
||||||
def update(self, can_parsers) -> tuple[structs.CarState, structs.IQCarState]:
|
def update(self, can_parsers) -> tuple[structs.CarState, structs.IQCarState]:
|
||||||
cp = can_parsers[Bus.pt]
|
cp = can_parsers[Bus.pt]
|
||||||
@@ -55,7 +61,9 @@ class CarState(CarStateBase):
|
|||||||
ret.steeringTorque = cp.vl["STEERING_TORQUE"]["DRIVER_TORQUE"]
|
ret.steeringTorque = cp.vl["STEERING_TORQUE"]["DRIVER_TORQUE"]
|
||||||
ret.steeringTorqueEps = cp.vl["STEERING_TORQUE"]["MAIN_TORQUE"]
|
ret.steeringTorqueEps = cp.vl["STEERING_TORQUE"]["MAIN_TORQUE"]
|
||||||
ret.steeringPressed = self.update_steering_pressed(abs(ret.steeringTorque) > CCP.STEER_DRIVER_OVERRIDE, 5)
|
ret.steeringPressed = self.update_steering_pressed(abs(ret.steeringTorque) > CCP.STEER_DRIVER_OVERRIDE, 5)
|
||||||
ret.steeringDisengage = abs(ret.steeringTorque) > CCP.STEER_DRIVER_DISENGAGE
|
# Disengagement on override is handled by the latch below, which also decides when
|
||||||
|
# re-engagement is allowed, so no separate hard-disengage threshold here.
|
||||||
|
ret.steeringDisengage = False
|
||||||
|
|
||||||
# state 11 is a latched dropout: the command stream stopped while the EPS was actuating.
|
# state 11 is a latched dropout: the command stream stopped while the EPS was actuating.
|
||||||
# It clears only on a STEER_REQ rising edge over a continuous stream.
|
# It clears only on a STEER_REQ rising edge over a continuous stream.
|
||||||
@@ -90,15 +98,35 @@ class CarState(CarStateBase):
|
|||||||
))
|
))
|
||||||
ret.seatbeltUnlatched = not bool(cp.vl["METER_CLUSTER"]["SEATBELT_DRIVER"])
|
ret.seatbeltUnlatched = not bool(cp.vl["METER_CLUSTER"]["SEATBELT_DRIVER"])
|
||||||
|
|
||||||
# ACC_STATE: 0=OFF, 2=AVAILABLE, 3=ACTIVE, 5=OVERRIDE, 7=ERROR
|
# The ADAS/ACC ECU is on the chassis bus, not behind the camera relay, so these come off
|
||||||
ret.cruiseState.speed = cp_cam.vl["ACC_HUD_ADAS"]["SET_SPEED"] * CV.KPH_TO_MS
|
# bus 0. Bus 2 carries only the camera's own frames (0x1E2, 0x316, ...). This differs from
|
||||||
acc_state = int(cp_cam.vl["ACC_HUD_ADAS"]["ACC_STATE"])
|
# the Atto 3, where PR #3337 reads both from the camera bus.
|
||||||
ret.cruiseState.available = acc_state in (2, 3, 5)
|
# CRUISE_STATE: 0=off, 1=available, 2=engaged, 3=engaged and commanding accel.
|
||||||
ret.cruiseState.enabled = acc_state in (3, 5)
|
# Do NOT use PR #3337/#3352's ACC_STATE (19|3) - byte 2 is a constant 0x3c on this car, so
|
||||||
ret.cruiseState.standstill = bool(cp_cam.vl["ACC_CMD"]["STANDSTILL_STATE"])
|
# it reads 7 (ERROR) forever and engagement can never happen.
|
||||||
|
ret.cruiseState.speed = cp.vl["ACC_HUD_ADAS"]["SET_SPEED"] * CV.KPH_TO_MS
|
||||||
|
cruise_state = int(cp.vl["ACC_HUD_ADAS"]["CRUISE_STATE"])
|
||||||
|
ret.cruiseState.available = cruise_state >= CRUISE_STATE_AVAILABLE
|
||||||
|
|
||||||
|
# A steering override fully disengages and stays disengaged. Re-arming is deliberate:
|
||||||
|
# either cycle stock cruise, or press the LKAS/ICC button. Suppressing cruiseState.enabled
|
||||||
|
# is what holds it off, and clearing the latch gives the rising edge that re-engages.
|
||||||
|
lkas_btn = bool(cp.vl["PCM_BUTTONS"]["LKAS_ON_BTN"])
|
||||||
|
lkas_rising = lkas_btn and not self.lkas_btn_prev
|
||||||
|
self.lkas_btn_prev = lkas_btn
|
||||||
|
|
||||||
|
# NOTE: latch on the instantaneous torque, not the debounced steeringPressed. byd_rx_hook
|
||||||
|
# latches on the same raw sample, and any skew between the two shows up as controlsMismatch.
|
||||||
|
if cruise_state < CRUISE_STATE_ENGAGED or lkas_rising:
|
||||||
|
self.override_latched = False
|
||||||
|
elif abs(ret.steeringTorque) > CCP.STEER_DRIVER_OVERRIDE:
|
||||||
|
self.override_latched = True
|
||||||
|
|
||||||
|
ret.cruiseState.enabled = cruise_state >= CRUISE_STATE_ENGAGED and not self.override_latched
|
||||||
|
ret.cruiseState.standstill = bool(cp.vl["ACC_CMD"]["STANDSTILL_STATE"])
|
||||||
|
|
||||||
self.lkas_hud = copy.copy(cp_cam.vl["LKAS_HUD_ADAS"])
|
self.lkas_hud = copy.copy(cp_cam.vl["LKAS_HUD_ADAS"])
|
||||||
self.acc_cmd = copy.copy(cp_cam.vl["ACC_CMD"])
|
self.acc_cmd = copy.copy(cp.vl["ACC_CMD"])
|
||||||
self.buttons = copy.copy(cp.vl["PCM_BUTTONS"])
|
self.buttons = copy.copy(cp.vl["PCM_BUTTONS"])
|
||||||
|
|
||||||
return ret, ret_iq
|
return ret, ret_iq
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from iqdbc.car import get_safety_config, structs
|
|||||||
from iqdbc.car.interfaces import CarInterfaceBase
|
from iqdbc.car.interfaces import CarInterfaceBase
|
||||||
from iqdbc.car.byd.carcontroller import CarController
|
from iqdbc.car.byd.carcontroller import CarController
|
||||||
from iqdbc.car.byd.carstate import CarState
|
from iqdbc.car.byd.carstate import CarState
|
||||||
from iqdbc.car.byd.values import BydSafetyFlags
|
from iqdbc.car.byd.values import BydFlags, BydSafetyFlags
|
||||||
|
|
||||||
|
|
||||||
class CarInterface(CarInterfaceBase):
|
class CarInterface(CarInterfaceBase):
|
||||||
@@ -23,8 +23,27 @@ class CarInterface(CarInterfaceBase):
|
|||||||
# through, so the panda never sees them
|
# through, so the panda never sees them
|
||||||
ret.radarUnavailable = True
|
ret.radarUnavailable = True
|
||||||
|
|
||||||
ret.alphaLongitudinalAvailable = True
|
# Two harness types exist for this car, and they differ in what can be filtered:
|
||||||
if alpha_long:
|
#
|
||||||
|
# camera harness - the relay only intercepts the MPC camera. 0x1E2/0x316 are camera
|
||||||
|
# frames so lateral works, but the ADAS/ACC ECU sits on the chassis bus
|
||||||
|
# in front of the relay: its 0x32E cannot be blocked and openpilot would
|
||||||
|
# contend with the stock ACC on the same address. Stock long only.
|
||||||
|
# gateway harness - the ACC ECU is behind the relay, so 0x32E is filterable and openpilot
|
||||||
|
# longitudinal is possible.
|
||||||
|
#
|
||||||
|
# These CANNOT be told apart from the fingerprint: fingerprinting runs with the relay
|
||||||
|
# closed, which ties bus 2 to bus 0, so bus 2 shows the whole car on either harness. The
|
||||||
|
# difference is only observable once the relay opens, which is after CarParams is fixed.
|
||||||
|
# Measured on a camera harness with the relay open: bus 2 carries 11 camera addresses and
|
||||||
|
# neither 0x32D nor 0x32E is among them.
|
||||||
|
#
|
||||||
|
# So default to the camera harness and keep longitudinal off. Setting GATEWAY_HARNESS is an
|
||||||
|
# explicit opt-in that must not be inferred - see BYD_SEALION7_PORT_PLAN.md.
|
||||||
|
gateway_harness = bool(ret.flags & BydFlags.GATEWAY_HARNESS)
|
||||||
|
|
||||||
|
ret.alphaLongitudinalAvailable = gateway_harness
|
||||||
|
if alpha_long and gateway_harness:
|
||||||
ret.openpilotLongitudinalControl = True
|
ret.openpilotLongitudinalControl = True
|
||||||
ret.safetyConfigs[0].safetyParam |= BydSafetyFlags.LONG_CONTROL.value
|
ret.safetyConfigs[0].safetyParam |= BydSafetyFlags.LONG_CONTROL.value
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
from iqdbc.can.packer import CANPacker
|
from iqdbc.can.packer import CANPacker
|
||||||
from iqdbc.can.parser import CANParser
|
from iqdbc.can.parser import CANParser
|
||||||
from iqdbc.car.byd import bydcan
|
from iqdbc.car.byd import bydcan
|
||||||
@@ -8,7 +10,7 @@ from iqdbc.car.byd.carstate import (EPS_STATE_OFF, EPS_STATE_PREPARED, EPS_STATE
|
|||||||
EPS_STATE_LATCHED_FAULT)
|
EPS_STATE_LATCHED_FAULT)
|
||||||
from iqdbc.car.byd.fingerprints import FW_VERSIONS
|
from iqdbc.car.byd.fingerprints import FW_VERSIONS
|
||||||
from iqdbc.car.byd.interface import CarInterface
|
from iqdbc.car.byd.interface import CarInterface
|
||||||
from iqdbc.car.byd.values import CAR, DBC, CarControllerParams
|
from iqdbc.car.byd.values import CAR, DBC, BydFlags, BydSafetyFlags, CarControllerParams
|
||||||
from iqdbc.car.fw_versions import match_fw_to_car_exact, build_fw_dict
|
from iqdbc.car.fw_versions import match_fw_to_car_exact, build_fw_dict
|
||||||
from iqdbc.car import structs
|
from iqdbc.car import structs
|
||||||
from iqdbc.car.structs import CarParams
|
from iqdbc.car.structs import CarParams
|
||||||
@@ -313,6 +315,99 @@ class TestBydCarController(unittest.TestCase):
|
|||||||
"0x32E sent while openpilotLongitudinalControl is off")
|
"0x32E sent while openpilotLongitudinalControl is off")
|
||||||
|
|
||||||
|
|
||||||
|
class TestBydLowSpeedAngleRate(unittest.TestCase):
|
||||||
|
"""Regression for the 2026-08-05 EPS latch. At 0.29 m/s the planner oscillated and the command
|
||||||
|
swung -5.9 to +2.4 deg against a stationary wheel in 220 ms; the EPS went from state 9 straight
|
||||||
|
to a latched 11 and took LKAS with it. The vehicle-model jerk limit cannot catch this because
|
||||||
|
it scales as 1/v^2."""
|
||||||
|
|
||||||
|
def _slew(self, v_ego, targets):
|
||||||
|
CP = CarInterface.get_non_essential_params("BYD_SEALION_7")
|
||||||
|
CP_IQ = CarInterface.get_non_essential_params_iq(CP, "BYD_SEALION_7")
|
||||||
|
cc = CarInterface.CarController({'pt': DBC_NAME}, CP, CP_IQ)
|
||||||
|
carstate = CarInterface.CarState(CP, CP_IQ)
|
||||||
|
cs_out, _ = carstate.update(CarInterface.CarState.get_can_parsers(CP, CP_IQ))
|
||||||
|
cs_out.vEgoRaw = v_ego
|
||||||
|
cs_out.vEgo = v_ego
|
||||||
|
cs_out.steeringAngleDeg = 0.1
|
||||||
|
|
||||||
|
class _CS:
|
||||||
|
pass
|
||||||
|
cs = _CS()
|
||||||
|
cs.out = cs_out
|
||||||
|
cs.lkas_hud = carstate.lkas_hud
|
||||||
|
cs.acc_cmd = carstate.acc_cmd
|
||||||
|
cs.buttons = carstate.buttons
|
||||||
|
|
||||||
|
CC_obj = structs.CarControl()
|
||||||
|
CC_obj.enabled = True
|
||||||
|
CC_obj.latActive = True
|
||||||
|
CC_IQ = structs.IQCarControl()
|
||||||
|
|
||||||
|
sent = []
|
||||||
|
for i, tgt in enumerate(targets):
|
||||||
|
CC_obj.actuators.steeringAngleDeg = tgt
|
||||||
|
cc.update(CC_obj.as_reader(), CC_IQ, cs, i * 10_000_000)
|
||||||
|
sent.append(cc.apply_angle_last)
|
||||||
|
return sent
|
||||||
|
|
||||||
|
# the actual planner output recorded during the fault
|
||||||
|
OSCILLATION = [-2.9, -5.9, -2.9, 0.1, 1.7, 1.8, 2.4, 2.2] * 3
|
||||||
|
|
||||||
|
def test_standstill_slew_is_bounded(self):
|
||||||
|
v = 0.29 # the speed at which the EPS latched
|
||||||
|
sent = self._slew(v, self.OSCILLATION)
|
||||||
|
cap = float(np.interp(v, CarControllerParams.ANGLE_RATE_BP, CarControllerParams.ANGLE_RATE_V))
|
||||||
|
steps = [abs(b - a) for a, b in zip(sent, sent[1:], strict=False)]
|
||||||
|
self.assertLessEqual(max(steps), cap + 1e-6,
|
||||||
|
"command slews faster than the standstill rate cap")
|
||||||
|
# the uncapped path stepped a full 3.0 deg/frame here
|
||||||
|
self.assertLess(cap, 1.0)
|
||||||
|
# and it must never wander far from the stationary wheel
|
||||||
|
self.assertLess(max(abs(a - 0.1) for a in sent), 2.0,
|
||||||
|
"command diverged from the measured angle at a standstill")
|
||||||
|
|
||||||
|
def test_rate_cap_scales_with_speed(self):
|
||||||
|
slow = self._slew(0.0, [30.0] * 10)
|
||||||
|
fast = self._slew(20.0, [30.0] * 10)
|
||||||
|
slow_step = max(abs(b - a) for a, b in zip(slow, slow[1:], strict=False))
|
||||||
|
fast_step = max(abs(b - a) for a, b in zip(fast, fast[1:], strict=False))
|
||||||
|
self.assertLess(slow_step, fast_step, "low-speed cap must be tighter than at speed")
|
||||||
|
self.assertLessEqual(fast_step, CarControllerParams.ANGLE_LIMITS.MAX_ANGLE_RATE + 1e-6)
|
||||||
|
|
||||||
|
|
||||||
|
class TestBydHarnessType(unittest.TestCase):
|
||||||
|
"""Longitudinal requires the ACC ECU to sit behind the relay so 0x32E is filterable. That is
|
||||||
|
a property of the harness, and it cannot be inferred from the fingerprint: fingerprinting
|
||||||
|
runs with the relay closed, which ties bus 2 to bus 0, so bus 2 shows the whole car either
|
||||||
|
way. Default must therefore be the camera harness (lateral only)."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _params(cam_bus_addrs, alpha_long=True):
|
||||||
|
fp = {0: {0x1FC: 8, 0x1F0: 8}, 1: {}, 2: dict.fromkeys(cam_bus_addrs, 8)}
|
||||||
|
return CarInterface.get_params("BYD_SEALION_7", fp, [], alpha_long, False, False)
|
||||||
|
|
||||||
|
def test_defaults_to_camera_harness_lateral_only(self):
|
||||||
|
CP = self._params([0x1E2, 0x316])
|
||||||
|
self.assertFalse(CP.flags & BydFlags.GATEWAY_HARNESS)
|
||||||
|
self.assertFalse(CP.alphaLongitudinalAvailable)
|
||||||
|
self.assertFalse(CP.openpilotLongitudinalControl)
|
||||||
|
self.assertFalse(CP.safetyConfigs[0].safetyParam & BydSafetyFlags.LONG_CONTROL)
|
||||||
|
|
||||||
|
def test_acc_cmd_on_fingerprint_bus2_does_not_imply_gateway(self):
|
||||||
|
# the relay is closed while fingerprinting, so bus 2 sees the chassis bus too. Seeing
|
||||||
|
# 0x32E there must NOT unlock longitudinal.
|
||||||
|
CP = self._params([0x1E2, 0x316, 0x32E, 0x32D, 0x1FC])
|
||||||
|
self.assertFalse(CP.flags & BydFlags.GATEWAY_HARNESS)
|
||||||
|
self.assertFalse(CP.alphaLongitudinalAvailable)
|
||||||
|
self.assertFalse(CP.openpilotLongitudinalControl)
|
||||||
|
|
||||||
|
def test_lateral_still_available_on_camera_harness(self):
|
||||||
|
CP = self._params([0x1E2, 0x316])
|
||||||
|
self.assertFalse(CP.dashcamOnly)
|
||||||
|
self.assertEqual(CP.steerControlType, CarParams.SteerControlType.angle)
|
||||||
|
|
||||||
|
|
||||||
class TestBydCarParams(unittest.TestCase):
|
class TestBydCarParams(unittest.TestCase):
|
||||||
def test_angle_control_and_no_radar(self):
|
def test_angle_control_and_no_radar(self):
|
||||||
CP = CarInterface.get_non_essential_params("BYD_SEALION_7")
|
CP = CarInterface.get_non_essential_params("BYD_SEALION_7")
|
||||||
|
|||||||
@@ -26,9 +26,31 @@ class CarControllerParams:
|
|||||||
MAX_ANGLE_RATE=3,
|
MAX_ANGLE_RATE=3,
|
||||||
)
|
)
|
||||||
|
|
||||||
# STEERING_TORQUE.DRIVER_TORQUE units (Nm). PROVISIONAL, re-derive in bring-up B8.
|
# Low-speed taper on the angle rate, in deg per STEER_STEP frame.
|
||||||
STEER_DRIVER_OVERRIDE = 3.0
|
#
|
||||||
STEER_DRIVER_DISENGAGE = 8.0
|
# The vehicle-model jerk limit scales as 1/v^2, so below a few m/s it stops binding and only
|
||||||
|
# the flat MAX_ANGLE_RATE is left. The lateral planner is ill-conditioned at a standstill and
|
||||||
|
# oscillates, and slewing the command at the full rate while the wheel is not moving walks the
|
||||||
|
# EPS straight from state 9 to a latched 11. Measured 2026-08-05 at 0.29 m/s: the command swung
|
||||||
|
# -5.9 to +2.4 deg in 220 ms against a stationary wheel and the EPS latched, taking LKAS with
|
||||||
|
# it. A healthy engagement at 0.9 m/s held the command within 1.1 deg of measured.
|
||||||
|
ANGLE_RATE_BP = [0.0, 2.0, 5.0] # m/s
|
||||||
|
ANGLE_RATE_V = [0.3, 1.0, 3.0] # deg/frame, tops out at MAX_ANGLE_RATE
|
||||||
|
|
||||||
|
# STEERING_TORQUE.DRIVER_TORQUE thresholds, derived from a drive where openpilot actually
|
||||||
|
# steered (route 0000000f, EPS state 10):
|
||||||
|
# |torque| while openpilot steered: p50 1.2 p90 2.7 p95 3.3 p99 5.8 max 9.8
|
||||||
|
# |torque| while the human drove: p50 0.2 p90 8.1 p95 17.2 p99 24.5 max 35.5
|
||||||
|
# The old 3.0 sat below what openpilot generates while steering, so it tripped its own
|
||||||
|
# override and dropped out within a few frames of every engage.
|
||||||
|
STEER_DRIVER_OVERRIDE = 12.0 # above openpilot's own max, below a deliberate grab
|
||||||
|
|
||||||
|
# Never command further than this from the actual wheel angle. The EPS latches state 11 on
|
||||||
|
# angle divergence, not just on a lost stream: measured 2026-08-05, the driver held the wheel
|
||||||
|
# at -13.4 deg while the controller wound the command out to -48.1 deg and the EPS latched at
|
||||||
|
# 34.7 deg of error. Normal closed-loop steering holds the error inside ~1.1 deg, so this only
|
||||||
|
# bites when the wheel is being physically held.
|
||||||
|
MAX_ANGLE_ERROR = 10.0 # deg
|
||||||
|
|
||||||
# comfort envelope, inside the safety cap of -3.5..+2.0
|
# comfort envelope, inside the safety cap of -3.5..+2.0
|
||||||
ACCEL_MIN = -3.0
|
ACCEL_MIN = -3.0
|
||||||
@@ -43,6 +65,16 @@ class BydSafetyFlags(IntFlag):
|
|||||||
LONG_CONTROL = 1
|
LONG_CONTROL = 1
|
||||||
|
|
||||||
|
|
||||||
|
class BydFlags(IntFlag):
|
||||||
|
# The ADAS/ACC ECU is behind the relay, so its 0x32E ACC_CMD can be blocked and replaced.
|
||||||
|
# Set when ACC_CMD is fingerprinted on the camera-side bus.
|
||||||
|
GATEWAY_HARNESS = 1
|
||||||
|
|
||||||
|
|
||||||
|
# addresses used to tell the two harness types apart
|
||||||
|
ACC_CMD_ADDR = 0x32E
|
||||||
|
|
||||||
|
|
||||||
class WMI(StrEnum):
|
class WMI(StrEnum):
|
||||||
BYD_AUTO = "LGX"
|
BYD_AUTO = "LGX"
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ BO_ 790 LKAS_HUD_ADAS: 8 MPC_CAM
|
|||||||
SG_ LKAS_REQ_PREPARE : 27|1@1+ (1,0) [0|1] "" MCU
|
SG_ LKAS_REQ_PREPARE : 27|1@1+ (1,0) [0|1] "" MCU
|
||||||
SG_ LKAS_ACTIVE : 28|1@1+ (1,0) [0|1] "" MCU
|
SG_ LKAS_ACTIVE : 28|1@1+ (1,0) [0|1] "" MCU
|
||||||
SG_ SLA_STATE : 29|3@1+ (1,0) [0|7] "" MCU
|
SG_ SLA_STATE : 29|3@1+ (1,0) [0|7] "" MCU
|
||||||
|
SG_ SET_ME_3 : 32|2@1+ (1,0) [0|3] "" MCU
|
||||||
SG_ RIGHT_LANE_STATE : 34|2@1+ (1,0) [0|3] "" MCU
|
SG_ RIGHT_LANE_STATE : 34|2@1+ (1,0) [0|3] "" MCU
|
||||||
SG_ LKAS_STATE : 36|4@1+ (1,0) [0|15] "" MCU
|
SG_ LKAS_STATE : 36|4@1+ (1,0) [0|15] "" MCU
|
||||||
SG_ SPEED_LIMIT_VALUE : 40|8@1+ (5,-5) [-5|1270] "kph" MCU
|
SG_ SPEED_LIMIT_VALUE : 40|8@1+ (5,-5) [-5|1270] "kph" MCU
|
||||||
@@ -122,7 +123,7 @@ BO_ 813 ACC_HUD_ADAS: 8 ADAS_ECU
|
|||||||
SG_ SET_DISTANCE : 12|3@0+ (1,0) [0|7] "" MCU
|
SG_ SET_DISTANCE : 12|3@0+ (1,0) [0|7] "" MCU
|
||||||
SG_ AEB : 16|1@1+ (1,0) [0|1] "" MCU
|
SG_ AEB : 16|1@1+ (1,0) [0|1] "" MCU
|
||||||
SG_ FCW : 17|1@1+ (1,0) [0|1] "" MCU
|
SG_ FCW : 17|1@1+ (1,0) [0|1] "" MCU
|
||||||
SG_ ACC_STATE : 19|3@1+ (1,0) [0|7] "" MCU
|
SG_ CRUISE_STATE : 44|4@1+ (1,0) [0|15] "" MCU
|
||||||
SG_ ACC_ON2 : 20|1@0+ (1,0) [0|1] "" MCU
|
SG_ ACC_ON2 : 20|1@0+ (1,0) [0|1] "" MCU
|
||||||
SG_ ACC_ON1 : 22|1@0+ (1,0) [0|1] "" MCU
|
SG_ ACC_ON1 : 22|1@0+ (1,0) [0|1] "" MCU
|
||||||
SG_ SET_ME_XFF : 47|8@0+ (1,0) [0|255] "" MCU
|
SG_ SET_ME_XFF : 47|8@0+ (1,0) [0|255] "" MCU
|
||||||
@@ -203,8 +204,11 @@ EPS state nibble = 8 + LKS_PREPARED + 2*CRUISE_ACTIVATED: 8 off, 9 prepared, 10
|
|||||||
here - never read state from it.";
|
here - never read state from it.";
|
||||||
|
|
||||||
CM_ BO_ 813 "SET_SPEED is 9 bits per HotIce0; bit 8 is unallocated in the 8-bit reading, so this
|
CM_ BO_ 813 "SET_SPEED is 9 bits per HotIce0; bit 8 is unallocated in the 8-bit reading, so this
|
||||||
is a safe superset. ACC_STATE bits are ACC_ON2 / ACC_CONTROLLABLE_AND_ON / OVERRIDE, giving
|
is a safe superset.
|
||||||
2=available, 3=active, 5=override, 7=error.";
|
CRUISE_STATE (44|4) is the real cruise state, NOT PR #3337/#3352's ACC_STATE at 19|3 - byte 2 is
|
||||||
|
a constant 0x3c on this car, so that field can only ever read 7 (ERROR) and engagement is
|
||||||
|
impossible. Measured over a 47 kph drive: 0=off, 1=available (ACC_CMD ACC_CONTROLLABLE_AND_ON
|
||||||
|
clear), 2=engaged (ON bit set, 100%), 3=engaged and commanding accel (mean +1.14 m/s^2).";
|
||||||
|
|
||||||
CM_ BO_ 814 "ACCEL_CMD in physical m/s^2, numerically identical to PR #3352's (1,-100) raw form.
|
CM_ BO_ 814 "ACCEL_CMD in physical m/s^2, numerically identical to PR #3352's (1,-100) raw form.
|
||||||
The SET_ME_25_*/ACCEL_FACTOR/DECEL_FACTOR split is kept because it is the form with real-car TX
|
The SET_ME_25_*/ACCEL_FACTOR/DECEL_FACTOR split is kept because it is the form with real-car TX
|
||||||
@@ -215,6 +219,9 @@ Sealion 7 in bring-up B4.";
|
|||||||
|
|
||||||
|
|
||||||
VAL_ 578 GEAR 1 "PARK" 2 "REVERSE" 3 "NEUTRAL" 4 "DRIVE" ;
|
VAL_ 578 GEAR 1 "PARK" 2 "REVERSE" 3 "NEUTRAL" 4 "DRIVE" ;
|
||||||
VAL_ 813 ACC_STATE 0 "OFF" 2 "AVAILABLE" 3 "ACTIVE" 5 "OVERRIDE" 7 "ERROR" ;
|
VAL_ 813 CRUISE_STATE 0 "OFF" 1 "AVAILABLE" 2 "ENGAGED" 3 "ENGAGED_ACCELERATING" ;
|
||||||
VAL_ 790 LKAS_STATE 0 "OFF" 1 "PASSIVE" 2 "ACTIVE" 3 "ACTIVE" 4 "FAULT" ;
|
VAL_ 790 LKAS_STATE 0 "OFF" 1 "PASSIVE" 2 "ACTIVE" 3 "ACTIVE" 4 "FAULT" ;
|
||||||
VAL_ 508 TORQUE_TEMP_FAILED 0 "OK" 1 "DERATE" 2 "DERATE" 3 "FAILED" ;
|
VAL_ 508 TORQUE_TEMP_FAILED 0 "OK" 1 "DERATE" 2 "DERATE" 3 "FAILED" ;
|
||||||
|
|
||||||
|
CM_ SG_ 790 SET_ME_3 "Bits 32-33. The camera sets these on every frame; they were undefined, so
|
||||||
|
the packer zeroed them and our regenerated 0x316 differed from stock. Passthrough only.";
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
#define BYD_WHEEL_SPEEDS 0x1F0U // RX from ESP, vehicle speed
|
#define BYD_WHEEL_SPEEDS 0x1F0U // RX from ESP, vehicle speed
|
||||||
#define BYD_DRIVE_STATE 0x242U // RX from VCU, gear + brake pressed
|
#define BYD_DRIVE_STATE 0x242U // RX from VCU, gear + brake pressed
|
||||||
#define BYD_PEDAL 0x342U // RX from VCU, accelerator pedal
|
#define BYD_PEDAL 0x342U // RX from VCU, accelerator pedal
|
||||||
#define BYD_ACC_HUD_ADAS 0x32DU // RX from ADAS(b2), cruise state
|
#define BYD_ACC_HUD_ADAS 0x32DU // RX from ADAS(b0), cruise state
|
||||||
#define BYD_STEERING_MODULE_ADAS 0x1E2U // TX to EPS, angle command
|
#define BYD_STEERING_MODULE_ADAS 0x1E2U // TX to EPS, angle command
|
||||||
#define BYD_LKAS_HUD_ADAS 0x316U // TX to cluster, LKAS HUD
|
#define BYD_LKAS_HUD_ADAS 0x316U // TX to cluster, LKAS HUD
|
||||||
#define BYD_ACC_CMD 0x32EU // TX to IPB, accel command
|
#define BYD_ACC_CMD 0x32EU // TX to IPB, accel command
|
||||||
@@ -18,6 +18,17 @@
|
|||||||
// WHEEL_SPEEDS scale, kph per LSB. PROVISIONAL - keep in lockstep with byd_sealion_7.dbc.
|
// WHEEL_SPEEDS scale, kph per LSB. PROVISIONAL - keep in lockstep with byd_sealion_7.dbc.
|
||||||
#define BYD_WHEEL_SPEED_SCALE 0.0725f
|
#define BYD_WHEEL_SPEED_SCALE 0.0725f
|
||||||
|
|
||||||
|
// STEERING_TORQUE.DRIVER_TORQUE counts (0.1 Nm/LSB). MUST equal CarControllerParams
|
||||||
|
// .STEER_DRIVER_OVERRIDE * 10 in values.py: carstate.py latches on the same instantaneous
|
||||||
|
// sample, and if the two sides disagree openpilot and the panda desync into controlsMismatch.
|
||||||
|
#define BYD_DRIVER_TORQUE_OVERRIDE 120
|
||||||
|
|
||||||
|
// A steering override disengages and stays disengaged until the driver deliberately re-arms,
|
||||||
|
// either by cycling stock cruise or pressing the LKAS/ICC button (0x3B0 bit 6, confirmed
|
||||||
|
// on-car). Mirrors the override latch in carstate.py.
|
||||||
|
static bool byd_override_latched = false;
|
||||||
|
static bool byd_lkas_btn_prev = false;
|
||||||
|
|
||||||
// ACC_CMD.ACCEL_CMD is an 8-bit field at 0.05 m/s^2 per LSB with a -5 m/s^2 offset, so raw 100
|
// ACC_CMD.ACCEL_CMD is an 8-bit field at 0.05 m/s^2 per LSB with a -5 m/s^2 offset, so raw 100
|
||||||
// is 0.0 m/s^2. Limits below are in offset-corrected LSBs.
|
// is 0.0 m/s^2. Limits below are in offset-corrected LSBs.
|
||||||
#define BYD_ACCEL_OFFSET 100
|
#define BYD_ACCEL_OFFSET 100
|
||||||
@@ -84,14 +95,35 @@ static void byd_rx_hook(const CANPacket_t *msg) {
|
|||||||
if (msg->addr == BYD_PEDAL) {
|
if (msg->addr == BYD_PEDAL) {
|
||||||
gas_pressed = msg->data[0] > 10U;
|
gas_pressed = msg->data[0] > 10U;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (msg->bus == 2U) {
|
// Driver torque, and the override latch. DRIVER_TORQUE is 4|12 signed.
|
||||||
// Cruise state. ACC_STATE is 3 bits starting at bit 19.
|
if (msg->addr == BYD_STEERING_TORQUE) {
|
||||||
// 0=OFF, 2=AVAILABLE, 3=ACTIVE, 5=OVERRIDE, 7=ERROR
|
int torque_driver_new = to_signed(((msg->data[1] & 0xFFU) << 4) | (msg->data[0] >> 4), 12);
|
||||||
|
update_sample(&torque_driver, torque_driver_new);
|
||||||
|
if (SAFETY_ABS(torque_driver_new) > BYD_DRIVER_TORQUE_OVERRIDE) {
|
||||||
|
byd_override_latched = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LKAS/ICC button (0x3B0 bit 6) re-arms after an override
|
||||||
|
if (msg->addr == BYD_PCM_BUTTONS) {
|
||||||
|
bool lkas_btn = ((msg->data[0] >> 6) & 0x1U) != 0U;
|
||||||
|
if (lkas_btn && !byd_lkas_btn_prev) {
|
||||||
|
byd_override_latched = false;
|
||||||
|
}
|
||||||
|
byd_lkas_btn_prev = lkas_btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cruise state. The ADAS/ACC ECU is on the chassis bus, not behind the camera relay.
|
||||||
|
// CRUISE_STATE is the high nibble of byte 5: 0=off, 1=available, 2=engaged,
|
||||||
|
// 3=engaged and commanding accel. PR #3337/#3352 read ACC_STATE from byte 2, which is a
|
||||||
|
// constant 0x3c here and can only ever report 7 (ERROR).
|
||||||
if (msg->addr == BYD_ACC_HUD_ADAS) {
|
if (msg->addr == BYD_ACC_HUD_ADAS) {
|
||||||
uint8_t acc_state = (msg->data[2] >> 3) & 0x7U;
|
uint8_t cruise_state = msg->data[5] >> 4;
|
||||||
bool acc_on = (acc_state == 3U) || (acc_state == 5U);
|
if (cruise_state < 2U) {
|
||||||
|
byd_override_latched = false;
|
||||||
|
}
|
||||||
|
bool acc_on = (cruise_state >= 2U) && !byd_override_latched;
|
||||||
pcm_cruise_check(acc_on);
|
pcm_cruise_check(acc_on);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,6 +185,10 @@ static safety_config byd_init(uint16_t param) {
|
|||||||
{BYD_PCM_BUTTONS, 0, 8, .check_relay = false},
|
{BYD_PCM_BUTTONS, 0, 8, .check_relay = false},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Longitudinal is only offered on a gateway harness, where the ACC ECU is behind the relay
|
||||||
|
// and 0x32E is genuinely filterable. On a camera harness the ACC ECU is in front of the
|
||||||
|
// relay, so alphaLongitudinalAvailable is false there and this list is never selected -
|
||||||
|
// check_relay would otherwise fire on every stock ACC frame.
|
||||||
static const CanMsg BYD_LONG_TX_MSGS[] = {
|
static const CanMsg BYD_LONG_TX_MSGS[] = {
|
||||||
{BYD_STEERING_MODULE_ADAS, 0, 8, .check_relay = true},
|
{BYD_STEERING_MODULE_ADAS, 0, 8, .check_relay = true},
|
||||||
{BYD_LKAS_HUD_ADAS, 0, 8, .check_relay = true},
|
{BYD_LKAS_HUD_ADAS, 0, 8, .check_relay = true},
|
||||||
@@ -168,9 +204,12 @@ static safety_config byd_init(uint16_t param) {
|
|||||||
{.msg = {{BYD_WHEEL_SPEEDS, 0, 8, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, // vehicle speed
|
{.msg = {{BYD_WHEEL_SPEEDS, 0, 8, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, // vehicle speed
|
||||||
{.msg = {{BYD_DRIVE_STATE, 0, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}}, // gear + brake (no counter/checksum)
|
{.msg = {{BYD_DRIVE_STATE, 0, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}}, // gear + brake (no counter/checksum)
|
||||||
{.msg = {{BYD_PEDAL, 0, 8, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, // accelerator pedal
|
{.msg = {{BYD_PEDAL, 0, 8, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, // accelerator pedal
|
||||||
{.msg = {{BYD_ACC_HUD_ADAS, 2, 8, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, // cruise state
|
{.msg = {{BYD_ACC_HUD_ADAS, 0, 8, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, // cruise state (chassis bus, not behind the relay)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
byd_override_latched = false;
|
||||||
|
byd_lkas_btn_prev = false;
|
||||||
|
|
||||||
bool byd_longitudinal = false;
|
bool byd_longitudinal = false;
|
||||||
|
|
||||||
SAFETY_UNUSED(param);
|
SAFETY_UNUSED(param);
|
||||||
|
|||||||
@@ -74,9 +74,21 @@ class TestBydSafetyBase(common.CarSafetyTest, common.AngleSteeringSafetyTest):
|
|||||||
return self.packer.make_can_msg_safety("STEER_MODULE_2", self.MAIN_BUS, values)
|
return self.packer.make_can_msg_safety("STEER_MODULE_2", self.MAIN_BUS, values)
|
||||||
|
|
||||||
def _pcm_status_msg(self, enable):
|
def _pcm_status_msg(self, enable):
|
||||||
# ACC_STATE: 0=OFF, 2=AVAILABLE, 3=ACTIVE, 5=OVERRIDE, 7=ERROR
|
# the ADAS/ACC ECU is on the chassis bus, not behind the camera relay
|
||||||
values = {"ACC_STATE": 3 if enable else 0}
|
# CRUISE_STATE: 0=off, 1=available, 2=engaged, 3=engaged and commanding accel
|
||||||
return self.packer.make_can_msg_safety("ACC_HUD_ADAS", self.CAM_BUS, values)
|
values = {"CRUISE_STATE": 2 if enable else 1}
|
||||||
|
return self.packer.make_can_msg_safety("ACC_HUD_ADAS", self.MAIN_BUS, values)
|
||||||
|
|
||||||
|
def test_cruise_state_not_read_from_constant_byte(self):
|
||||||
|
# PR #3337/#3352 read ACC_STATE from byte 2, which is constant 0x3c on this car. Setting
|
||||||
|
# only that byte must never enable cruise.
|
||||||
|
self.safety.set_controls_allowed(0)
|
||||||
|
for _ in range(5):
|
||||||
|
self._rx(self.packer.make_can_msg_safety("ACC_HUD_ADAS", self.MAIN_BUS, {"CRUISE_STATE": 0}))
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed())
|
||||||
|
for _ in range(5):
|
||||||
|
self._rx(self.packer.make_can_msg_safety("ACC_HUD_ADAS", self.MAIN_BUS, {"CRUISE_STATE": 3}))
|
||||||
|
self.assertTrue(self.safety.get_controls_allowed())
|
||||||
|
|
||||||
def _speed_msg(self, speed):
|
def _speed_msg(self, speed):
|
||||||
# all four wheels, matching the rx hook's average
|
# all four wheels, matching the rx hook's average
|
||||||
@@ -179,6 +191,7 @@ class TestBydStockSafety(TestBydSafetyBase):
|
|||||||
|
|
||||||
class TestBydLongSafety(TestBydSafetyBase, common.LongitudinalAccelSafetyTest):
|
class TestBydLongSafety(TestBydSafetyBase, common.LongitudinalAccelSafetyTest):
|
||||||
TX_MSGS = [[STEERING_MODULE_ADAS, 0], [LKAS_HUD_ADAS, 0], [ACC_CMD, 0], [PCM_BUTTONS, 0]]
|
TX_MSGS = [[STEERING_MODULE_ADAS, 0], [LKAS_HUD_ADAS, 0], [ACC_CMD, 0], [PCM_BUTTONS, 0]]
|
||||||
|
# long is only offered on a gateway harness, where 0x32E is behind the relay
|
||||||
RELAY_MALFUNCTION_ADDRS = {0: (STEERING_MODULE_ADAS, LKAS_HUD_ADAS, ACC_CMD)}
|
RELAY_MALFUNCTION_ADDRS = {0: (STEERING_MODULE_ADAS, LKAS_HUD_ADAS, ACC_CMD)}
|
||||||
FWD_BLACKLISTED_ADDRS = {2: [STEERING_MODULE_ADAS, LKAS_HUD_ADAS, ACC_CMD]}
|
FWD_BLACKLISTED_ADDRS = {2: [STEERING_MODULE_ADAS, LKAS_HUD_ADAS, ACC_CMD]}
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,10 @@ void init_interrupts(bool check_rate_limit){
|
|||||||
|
|
||||||
for(uint16_t i=0U; i<NUM_INTERRUPTS; i++){
|
for(uint16_t i=0U; i<NUM_INTERRUPTS; i++){
|
||||||
interrupts[i].handler = unused_interrupt_handler;
|
interrupts[i].handler = unused_interrupt_handler;
|
||||||
|
// Default priority, lowered so the comms link can preempt everything else and
|
||||||
|
// re-arm its DMA (see IRQ_PRIORITY_COMMS). Shared state is guarded by
|
||||||
|
// ENTER_CRITICAL, which masks all interrupts regardless of priority.
|
||||||
|
NVIC_SetPriority((IRQn_Type)i, IRQ_PRIORITY_DEFAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init interrupt timer for a 1s interval
|
// Init interrupt timer for a 1s interval
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
// The SPI slave must re-arm its RX DMA at every protocol turnaround before the
|
||||||
|
// master clocks the next phase. Without preemption that re-arm waits behind any
|
||||||
|
// in-flight handler (CAN RX under bus load), the master clocks into an unarmed
|
||||||
|
// peripheral, and the transfer fails its checksum -> NACK retry storms.
|
||||||
|
#define IRQ_PRIORITY_COMMS 0U
|
||||||
|
#define IRQ_PRIORITY_DEFAULT 2U
|
||||||
|
|
||||||
typedef struct interrupt {
|
typedef struct interrupt {
|
||||||
IRQn_Type irq_type;
|
IRQn_Type irq_type;
|
||||||
void (*handler)(void);
|
void (*handler)(void);
|
||||||
|
|||||||
@@ -101,6 +101,12 @@ void llspi_init(void) {
|
|||||||
register_set(&(SPI4->CR1), SPI_CR1_SPE, 0xFFFFU);
|
register_set(&(SPI4->CR1), SPI_CR1_SPE, 0xFFFFU);
|
||||||
register_set(&(SPI4->CR2), 0, 0xFFFFU);
|
register_set(&(SPI4->CR2), 0, 0xFFFFU);
|
||||||
|
|
||||||
|
// preempt other handlers so the RX DMA is re-armed before the master clocks
|
||||||
|
// the next phase of a transfer
|
||||||
|
NVIC_SetPriority(DMA2_Stream2_IRQn, IRQ_PRIORITY_COMMS);
|
||||||
|
NVIC_SetPriority(DMA2_Stream3_IRQn, IRQ_PRIORITY_COMMS);
|
||||||
|
NVIC_SetPriority(SPI4_IRQn, IRQ_PRIORITY_COMMS);
|
||||||
|
|
||||||
NVIC_EnableIRQ(DMA2_Stream2_IRQn);
|
NVIC_EnableIRQ(DMA2_Stream2_IRQn);
|
||||||
NVIC_EnableIRQ(DMA2_Stream3_IRQn);
|
NVIC_EnableIRQ(DMA2_Stream3_IRQn);
|
||||||
NVIC_EnableIRQ(SPI4_IRQn);
|
NVIC_EnableIRQ(SPI4_IRQn);
|
||||||
|
|||||||
Reference in New Issue
Block a user