"""A set of helper functions to the new busparam API added in CANlib v5.34.
.. versionadded :: v1.16
"""
import collections
SI_PREFIXES = {
24: 'Y',
21: 'Z',
18: 'E',
15: 'P',
12: 'T',
9: 'G',
6: 'M',
3: 'k',
0: '',
-3: 'm',
-6: 'u',
-9: 'n',
-12: 'p',
-15: 'f',
-18: 'a',
-21: 'z',
-24: 'y',
}
[docs]def calc_bitrate(target_bitrate, clk_freq):
"""Calculate nearest available bitrate
Args:
target_bitrate (`int`): Wanted bitrate (bit/s)
clk_freq (`int`): Device clock frequency (Hz)
Returns:
The returned tuple is a ``(bitrate, tq)`` named tuple of:
#. ``bitrate`` (`int`): Available bitrate, could be a rounded value (bit/s)
#. ``tq`` (`int`): Number of time quanta in one bit
.. versionadded :: v1.16
"""
tq = round(clk_freq / target_bitrate)
# Minimum tq is 3 (sync_seg + phase1 + phase2)
if tq < 3:
tq = 3
bitrate = clk_freq / tq
Bitrate = collections.namedtuple('Bitrate', 'bitrate tq')
return Bitrate(bitrate=bitrate, tq=tq)
[docs]def calc_busparamstq(target_bitrate,
target_sample_point,
target_sync_jump_width,
clk_freq,
target_prop_tq=None):
"""Calculate closest matching busparameters.
The device clock frequency, `clk_freq`, can be obtained via
`ChannelData.clock_info.frequency()`:
>>> chd = canlib.ChannelData(channel_number=0)
>>> clock_info = chd.clock_info
>>> clock_info.frequency()
80000000.0
Now call `calc_busparamstq` with target values, and a `BusParamsTq`
object will be returned:
>>> params = calc_busparamstq(
... target_bitrate=470_000,
... target_sample_point=82,
... target_sync_jump_width=33.5,
... clk_freq=clock_info.frequency())
>>> params
BusParamsTq(tq=170, prop=107, phase1=31, phase2=31, sjw=57, prescaler=1)
Note: Paramters are calculated with prescaler = 1, minimum sjw returned is 1.
Args:
target_bitrate (`float`): Wanted bitrate (bit/s)
target_sample_point (`float`): Wanted sample point in percentage (0-100)
target_sync_jump_width (`float`): Wanted sync jump width, 0-100 (%)
clk_freq (`float`): Device clock frequency (Hz)
target_prop_tq(`int`, Optional): Wanted propagation segment (time quanta)
Returns:
`BusParamsTq`: Calculated bus parameters
.. versionadded :: v1.16
"""
bitrate, tq = calc_bitrate(target_bitrate, clk_freq)
prescaler = 1
sjw, sync_jump_width = calc_sjw(tq, target_sync_jump_width)
sample_point, phase2 = calc_sample_point(tq, target_sample_point)
sync_tq = 1
if target_prop_tq is None:
phase1 = phase2
else:
phase1 = calc_phase_seg1(tq, target_prop_tq, phase2, sync_tq)
if phase1 < sjw:
phase1 = sjw
prop = calc_prop_seg(tq, phase1, phase2, sync_tq)
return BusParamsTq(
tq=tq,
prop=prop,
phase1=phase1,
phase2=phase2,
sjw=sjw,
prescaler=prescaler,
)
def calc_phase_seg1(tq, prop, phase2, sync_tq=1):
"""Calculate phase segment 1"""
return tq - prop - phase2 - sync_tq
def calc_prop_seg(tq, phase1, phase2, sync_tq=1):
"""Calculate propagation segment"""
return tq - phase2 - phase1 - sync_tq
def calc_sample_point(tq, target_sample_point):
"""Calculate actual sample_point and phase segment 2
tq (`int`): Total number of quanta in one bit
target_sample_point (`float`): Wanted sample point in percentage (0-100)
Returns:
named tuple(sample_point, phase2)
phase2: Number of time quanta in phase segment 2
sample_point: Sample point in percentage
.. versionadded :: v1.16
"""
phase2 = tq - (round((target_sample_point / 100) * tq))
sample_point = ((tq - phase2) / tq) * 100
SamplePoint = collections.namedtuple('SamplePoint', 'sample_point phase2')
return SamplePoint(sample_point=sample_point, phase2=phase2)
def calc_sjw(tq, target_sync_jump_width):
"""Calculate sync jump width
tq: Number of time quanta in one bit
target_sync_jump_width: Wanted sync jump width, 0-100 (%)
Note: Minimum sjw_tq returned is 1.
Returns:
`namedtuple`(sjw_tq, sync_jump_width)
sjw_tq: Size of sync jump width in number of time quanta
sync_jump_width: Size of sync jump width in percentage (%)
.. versionadded :: v1.16
"""
sjw_tq = round((target_sync_jump_width / 100) * tq)
if sjw_tq < 1:
sjw_tq = 1
sync_jump_width = round(sjw_tq / tq * 100)
Sjw = collections.namedtuple('Sjw', 'sjw_tq sync_jump_width')
return Sjw(sjw_tq=sjw_tq, sync_jump_width=sync_jump_width)
[docs]class ClockInfo:
"""Information about clock a oscillator
The clock frequency is set in the form:
frequency = numerator / denominator * 10 ** power_of_ten +/- accuracy
.. versionadded :: v1.16
"""
@classmethod
def from_list(cls, args):
"""Create a `ClockInfo` object from a versioned list of values.
The first number in the list is the version number for the list and
must be 1. See the `kvClockInfo` structure in CANlib for more
information.
Args:
args (`list` (version (`int`), ...)): where version must be 1.
Returns:
`ClockInfo`
"""
version = args[0]
if version == 1:
return ClockInfo(
numerator=args[1],
denominator=args[2],
power_of_ten=args[3],
accuracy=args[4],
)
else:
raise ValueError(
"Clock info version '{v}' is unknown, perhaps update canlib?".format(v=version)
)
def __init__(self, numerator, denominator, power_of_ten, accuracy):
self.numerator = numerator
self.denominator = denominator
self.power_of_ten = power_of_ten
self.accuracy = accuracy
def __eq__(self, other):
if not isinstance(other, ClockInfo):
return NotImplemented
if self.accuracy != other.accuracy:
return False
if self.frequency() != other.frequency():
return False
return True
def __repr__(self):
txt = (
f"ClockInfo(numerator={self.numerator}, denominator={self.denominator}, "
f"power_of_ten={self.power_of_ten}, accuracy={self.accuracy})"
)
return txt
def __str__(self):
frequency = self.frequency()
if self.power_of_ten in SI_PREFIXES:
frequency = frequency / 10 ** self.power_of_ten
si_prefix = SI_PREFIXES[self.power_of_ten]
else:
si_prefix = ""
txt = self.__repr__() + f", (frequency: {frequency} {si_prefix}Hz)"
return txt
[docs] def frequency(self):
"""Returns an approximation of the clock frequency as a float."""
return self.numerator / self.denominator * 10 ** self.power_of_ten
[docs]class BusParamsTq:
"""Holds parameters for busparameters in number of time quanta.
If you don't want to specify the busparameters in time quanta directly,
you may use `calc_busparamstq` which returns an object of this class.
>>> params = calc_busparamstq(
... target_bitrate=470_000,
... target_sample_point=82,
... target_sync_jump_width=33.5,
... clk_freq=clk_freq)
>>> params
BusParamsTq(tq=170, prop=107, phase1=31, phase2=31, sjw=57, prescaler=1)
You may now query for the actual Sample Point and Sync Jump Width expressed
as percentages of total bit time quanta:
>>> params.sample_point()
81.76470588235294
>>> params.sync_jump_width()
33.52941176470588
If you supply the clock frequency, you may also calculate the corresponding bitrate:
>>> params.bitrate(clk_freq=80_000_000)
470588.23529411765
Args:
tq (`int`): Number of time quanta in one bit.
phase1 (`int`): Number of time quanta in Phase Segment 1.
phase2 (`int`): Number of time quanta in Phase Segment 2.
sjw (`int`): Number of time quanta in Sync Jump Width.
prescaler (`int`): Prescaler value (1-2 to enable auto in CAN FD)
prop (`int`, optional): Number of time quanta in Propagation Segment.
.. versionadded :: v1.16
"""
def __init__(self, tq, phase1, phase2, sjw, prescaler=1, prop=None):
self.sync = 1
if prop is None:
self.prop = tq - phase1 - phase2 - self.sync
else:
self.prop = prop
if tq != self.sync + self.prop + phase1 + phase2:
raise ValueError(
f"Total number of time quanta does not match: {tq}(tq) !="
f" {self.sync}(sync) + {self.prop}(prop) + {phase1}(phase1) + {phase2}(phase2)"
)
self.tq = tq
self.phase1 = phase1
self.phase2 = phase2
self.sjw = sjw
self.prescaler = prescaler
def __eq__(self, other):
if not isinstance(other, BusParamsTq):
return NotImplemented
if self.tq != other.tq:
return False
if self.phase1 != other.phase1:
return False
if self.phase2 != other.phase2:
return False
if self.sjw != other.sjw:
return False
if self.prescaler != other.prescaler:
return False
if self.prop != other.prop:
return False
return True
def __repr__(self):
txt = (f"BusParamsTq(tq={self.tq}, prop={self.prop}, phase1={self.phase1},"
f" phase2={self.phase2}, sjw={self.sjw}, prescaler={self.prescaler})")
return txt
[docs] def bitrate(self, clk_freq):
"""Return bitrate assuming the given clock frequency
Args:
clk_freq (`int`): Clock frequency (in Hz)
"""
return clk_freq / self.tq
[docs] def sample_point(self):
"""Return sample point in percentage."""
return ((self.tq - self.phase2) / self.tq) * 100
[docs] def sync_jump_width(self):
"""Return sync jump width (SJW) in percentage."""
return self.sjw / self.tq * 100
def calc_tolerance(nominal, data=None):
"""Calculate tolerance with the given `BusParamsTq`.
Args:
nominal (`BusParamsTq`): Nominal bus parameters
data (`BusParamsTq`, optional): Bus parameters for data phase (in CAN FD)
Returns:
`namedtuple`(df1, df2, df3, df4, df5)
.. versionadded :: v1.16
"""
df1 = nominal.sjw / (2 * 10 * nominal.tq)
df1 = round((df1 * 2) * 1000000)
min_Tphase_seg = min(nominal.phase1, nominal.phase2)
df2 = min_Tphase_seg / ((13 * nominal.tq) - nominal.phase2)
df2 = round(df2 * 1000000)
if data is None:
df3 = None
df4 = None
df5 = None
else:
df3 = data.sjw / (10 * data.tq)
df3 = round(df3 * 1000000)
d_n_ratio = data.prescaler / nominal.prescaler
df4 = (min_Tphase_seg / (6 * data.tq - data.phase2
* d_n_ratio + 7 * nominal.tq))
df4 = round(df4 * 1000000)
n_d_ratio = nominal.prescaler / data.prescaler
df5 = ((data.sjw - max(0, n_d_ratio - 1)) /
((2 * nominal.tq - nominal.phase2) *
n_d_ratio + data.phase2 + 4 * data.tq))
df5 = round(df5 * 1000000)
Tolerance = collections.namedtuple('Tolerance', 'df1 df2 df3 df4 df5')
return Tolerance(df1=df1, df2=df2, df3=df3, df4=df4, df5=df5)