Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 101 additions & 28 deletions can/bit_timing.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,19 +74,30 @@ def __init__(
"sjw": sjw,
"nof_samples": nof_samples,
}
self._tseg1_max = max(16, tseg1)
self._tseg2_max = max(8, tseg2)
self._brp_max = max(64, brp)
if strict:
self._validate()
self._restrict_to_minimum_range()

def _validate(self) -> None:
if not 1 <= self.brp <= 64:
raise ValueError(f"bitrate prescaler (={self.brp}) must be in [1...64].")
def _validate(
self,
*,
tseg1_max: int = 16,
tseg2_max: int = 8,
brp_max: int = 64,
) -> None:
if not 1 <= self.brp <= brp_max:
raise ValueError(
f"bitrate prescaler (={self.brp}) must be in [1...{brp_max}]."
)

if not 1 <= self.tseg1 <= 16:
raise ValueError(f"tseg1 (={self.tseg1}) must be in [1...16].")
if not 1 <= self.tseg1 <= tseg1_max:
raise ValueError(f"tseg1 (={self.tseg1}) must be in [1...{tseg1_max}].")

if not 1 <= self.tseg2 <= 8:
raise ValueError(f"tseg2 (={self.tseg2}) must be in [1...8].")
if not 1 <= self.tseg2 <= tseg2_max:
raise ValueError(f"tseg2 (={self.tseg2}) must be in [1...{tseg2_max}].")

if not 1 <= self.sjw <= 4:
raise ValueError(f"sjw (={self.sjw}) must be in [1...4].")
Expand Down Expand Up @@ -215,7 +226,14 @@ def from_registers(

@classmethod
def iterate_from_sample_point(
cls, f_clock: int, bitrate: int, sample_point: float = 69.0
cls,
f_clock: int,
bitrate: int,
sample_point: float = 69.0,
*,
tseg1_max: int = 16,
tseg2_max: int = 8,
brp_max: int = 64,
) -> Iterator["BitTiming"]:
"""Create a :class:`~can.BitTiming` iterator with all the solutions for a sample point.

Expand All @@ -225,45 +243,74 @@ def iterate_from_sample_point(
Bitrate in bit/s.
:param int sample_point:
The sample point value in percent.
:param int tseg1_max:
Maximum time segment 1 value supported by the CAN controller.
:param int tseg2_max:
Maximum time segment 2 value supported by the CAN controller.
:param int brp_max:
Maximum bit rate prescaler supported by the CAN controller.
:raises ValueError:
if the arguments are invalid.
"""

if sample_point < 50.0:
raise ValueError(f"sample_point (={sample_point}) must not be below 50%.")

for brp in range(1, 65):
for name, value in (
("tseg1_max", tseg1_max),
("tseg2_max", tseg2_max),
("brp_max", brp_max),
):
if value < 1:
raise ValueError(f"{name} (={value}) must be at least 1.")

if not 5_000 <= bitrate <= 1_000_000:
return
Comment thread
timothyanderson096-ocdealcheck marked this conversation as resolved.

for brp in range(1, brp_max + 1):
nbt = int(f_clock / (bitrate * brp))
if nbt < 8:
break

effective_bitrate = f_clock / (nbt * brp)
if abs(effective_bitrate - bitrate) > bitrate / 256:
continue
if not 5_000 <= round(effective_bitrate) <= 1_000_000:
continue

tseg1_min = max(1, nbt - tseg2_max - 1)
tseg1_max_feasible = min(tseg1_max, nbt - 2)
if tseg1_min > tseg1_max_feasible:
continue

tseg1 = round(sample_point / 100 * nbt) - 1
# limit tseg1, so tseg2 is at least 1 TQ
tseg1 = min(tseg1, nbt - 2)
tseg1 = max(tseg1_min, min(tseg1, tseg1_max_feasible))

tseg2 = nbt - tseg1 - 1
sjw = min(tseg2, 4)

try:
bt = BitTiming(
f_clock=f_clock,
brp=brp,
tseg1=tseg1,
tseg2=tseg2,
sjw=sjw,
strict=True,
)
yield bt
except ValueError:
continue
timing = cls(
f_clock=f_clock,
brp=brp,
tseg1=tseg1,
tseg2=tseg2,
sjw=sjw,
)
timing._tseg1_max = tseg1_max
timing._tseg2_max = tseg2_max
timing._brp_max = brp_max
yield timing

@classmethod
def from_sample_point(
cls, f_clock: int, bitrate: int, sample_point: float = 69.0
cls,
f_clock: int,
bitrate: int,
sample_point: float = 69.0,
*,
tseg1_max: int = 16,
tseg2_max: int = 8,
brp_max: int = 64,
) -> "BitTiming":
"""Create a :class:`~can.BitTiming` instance for a sample point.

Expand All @@ -280,6 +327,12 @@ def from_sample_point(
Bitrate in bit/s.
:param int sample_point:
The sample point value in percent.
:param int tseg1_max:
Maximum time segment 1 value supported by the CAN controller.
:param int tseg2_max:
Maximum time segment 2 value supported by the CAN controller.
:param int brp_max:
Maximum bit rate prescaler supported by the CAN controller.
:raises ValueError:
if the arguments are invalid.
"""
Expand All @@ -288,7 +341,14 @@ def from_sample_point(
raise ValueError(f"sample_point (={sample_point}) must not be below 50%.")

possible_solutions: list[BitTiming] = list(
cls.iterate_from_sample_point(f_clock, bitrate, sample_point)
cls.iterate_from_sample_point(
f_clock,
bitrate,
sample_point,
tseg1_max=tseg1_max,
tseg2_max=tseg2_max,
brp_max=brp_max,
)
)

if not possible_solutions:
Expand Down Expand Up @@ -413,7 +473,7 @@ def recreate_with_f_clock(self, f_clock: int) -> "BitTiming":
"""
# try the most simple solution first: another bitrate prescaler
try:
return BitTiming.from_bitrate_and_segments(
bt = BitTiming.from_bitrate_and_segments(
f_clock=f_clock,
bitrate=self.bitrate,
tseg1=self.tseg1,
Expand All @@ -422,12 +482,21 @@ def recreate_with_f_clock(self, f_clock: int) -> "BitTiming":
nof_samples=self.nof_samples,
strict=True,
)
bt._tseg1_max = self._tseg1_max
bt._tseg2_max = self._tseg2_max
bt._brp_max = self._brp_max
return bt
except ValueError:
pass

# create a new timing instance with the same sample point
bt = BitTiming.from_sample_point(
f_clock=f_clock, bitrate=self.bitrate, sample_point=self.sample_point
f_clock=f_clock,
bitrate=self.bitrate,
sample_point=self.sample_point,
tseg1_max=self._tseg1_max,
tseg2_max=self._tseg2_max,
brp_max=self._brp_max,
)
if abs(bt.sample_point - self.sample_point) > 1.0:
raise ValueError(
Expand All @@ -438,7 +507,11 @@ def recreate_with_f_clock(self, f_clock: int) -> "BitTiming":
sjw = max(1, min(4, bt.tseg2, sjw))
bt._data["sjw"] = sjw # pylint: disable=protected-access
bt._data["nof_samples"] = self.nof_samples # pylint: disable=protected-access
bt._validate() # pylint: disable=protected-access
bt._validate( # pylint: disable=protected-access
tseg1_max=self._tseg1_max,
tseg2_max=self._tseg2_max,
brp_max=self._brp_max,
)
return bt

def __str__(self) -> str:
Expand Down
14 changes: 14 additions & 0 deletions doc/bit_timing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,20 @@ produce an overview of possible bit timings for your desired bit rate:
BR: 250_000 bit/s, SP: 87.50%, BRP: 2, TSEG1: 13, TSEG2: 2, SJW: 2, BTR: 411Ch, CLK: 8MHz
BR: 250_000 bit/s, SP: 93.75%, BRP: 2, TSEG1: 14, TSEG2: 1, SJW: 1, BTR: 010Dh, CLK: 8MHz

Controller-specific maximum values can be supplied when the standard timing
limits are too restrictive:

.. code-block:: python

timing = can.BitTiming.from_sample_point(
f_clock=160_000_000,
bitrate=250_000,
sample_point=87.5,
tseg1_max=256,
tseg2_max=128,
brp_max=512,
)


It is possible to specify CAN 2.0 bit timings
using the config file:
Expand Down
1 change: 1 addition & 0 deletions doc/changelog.d/2083.fixed.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow ``BitTiming.from_sample_point`` to find valid timings for CAN controllers with bit rate prescalers above 32.
101 changes: 101 additions & 0 deletions test/test_bit_timing.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,88 @@ def test_from_sample_point():
)


def test_from_sample_point_with_extended_hardware_limits():
timing = can.BitTiming.from_sample_point(
f_clock=160_000_000,
bitrate=250_000,
sample_point=87.5,
)

assert timing.bitrate == 250_000
assert timing.sample_point == 87.5
assert timing.brp == 40
assert timing.tseg1 == 13
assert timing.tseg2 == 2

extended_timing = can.BitTiming.from_sample_point(
f_clock=160_000_000,
bitrate=250_000,
sample_point=87.5,
tseg1_max=256,
tseg2_max=128,
brp_max=512,
)
assert extended_timing.brp == 4
assert extended_timing.tseg1 == 139
assert extended_timing.tseg2 == 20

with pytest.raises(ValueError, match="No suitable bit timings found"):
can.BitTiming.from_sample_point(
f_clock=160_000_000,
bitrate=250_000,
sample_point=87.5,
brp_max=28,
)

with pytest.raises(ValueError, match="No suitable bit timings found"):
can.BitTiming.from_sample_point(
f_clock=80_000_000,
bitrate=2_000_000,
sample_point=75.0,
)

for parameter in ("tseg1_max", "tseg2_max", "brp_max"):
with pytest.raises(ValueError, match=rf"{parameter} \(=0\) must be at least 1"):
list(
can.BitTiming.iterate_from_sample_point(
f_clock=16_000_000,
bitrate=500_000,
**{parameter: 0},
)
)


def test_from_sample_point_rejects_effective_bitrate_outside_supported_range():
with pytest.raises(ValueError, match="No suitable bit timings found"):
can.BitTiming.from_sample_point(
f_clock=8_024_000,
bitrate=1_000_000,
sample_point=75.0,
)


@pytest.mark.parametrize(
("limits", "expected_tseg1", "expected_tseg2"),
[
({"tseg1_max": 4}, 4, 3),
({"tseg2_max": 1}, 6, 1),
],
)
def test_from_sample_point_clamps_to_controller_segment_limits(
limits, expected_tseg1, expected_tseg2
):
timing = can.BitTiming.from_sample_point(
f_clock=8_000_000,
bitrate=1_000_000,
sample_point=75.0,
**limits,
)

assert timing.bitrate == 1_000_000
assert timing.tseg1 == expected_tseg1
assert timing.tseg2 == expected_tseg2


def test_iterate_from_sample_point():
for sp in range(50, 100):
solutions = list(
Expand Down Expand Up @@ -492,6 +574,25 @@ def test_recreate_with_f_clock():
)
assert timing_8mhz.nof_samples == timing_16mhz.nof_samples

extended_timing_160mhz = can.BitTiming.from_sample_point(
f_clock=160_000_000,
bitrate=5_000,
sample_point=75.0,
tseg1_max=256,
tseg2_max=128,
brp_max=512,
)
extended_timing_200mhz = extended_timing_160mhz.recreate_with_f_clock(
f_clock=200_000_000
)
assert (
abs(extended_timing_200mhz.bitrate - extended_timing_160mhz.bitrate)
<= extended_timing_160mhz.bitrate / 256
)
assert extended_timing_200mhz.sample_point == pytest.approx(
extended_timing_160mhz.sample_point, abs=1.0
)

timing_16mhz = can.BitTiming(
f_clock=16000000, brp=2, tseg1=12, tseg2=3, sjw=3, nof_samples=1
)
Expand Down