Skip to content

Commit 865866d

Browse files
author
David Peverley
committed
socketcan: support use of SO_TIMESTAMPING for hardware timestamps
The current implemenation of socketcan utilises SO_TIMESTAMPNS which only offers system timestamps. I've looked at how can-utils candump.c configures hardware timestamping and implemented this in socketcan as a new option 'can_hardware_timestamps' which is disabled by default to avoid any potential adverse impact on existing usage. Additionally modify logger.py to provide an additional '-H' flag in the same way that candump does in order to use this functionality.
1 parent 5be89ec commit 865866d

File tree

3 files changed

+63
-11
lines changed

3 files changed

+63
-11
lines changed

can/interfaces/socketcan/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
# Generic socket constants
66
SO_TIMESTAMPNS = 35
7+
SO_TIMESTAMPING = 37
8+
SOF_TIMESTAMPING_RX_SOFTWARE = 1 << 3
9+
SOF_TIMESTAMPING_SOFTWARE = 1 << 4
10+
SOF_TIMESTAMPING_RAW_HARDWARE = 1 << 6
711

812
CAN_ERR_FLAG = 0x20000000
913
CAN_RTR_FLAG = 0x40000000

can/interfaces/socketcan/socketcan.py

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@
4242

4343

4444
# Constants needed for precise handling of timestamps
45-
RECEIVED_TIMESTAMP_STRUCT = struct.Struct("@ll")
45+
RECEIVED_TIMESPEC_STRUCT = struct.Struct("@ll")
4646
RECEIVED_ANCILLARY_BUFFER_SIZE = (
47-
CMSG_SPACE(RECEIVED_TIMESTAMP_STRUCT.size) if CMSG_SPACE_available else 0
47+
CMSG_SPACE(RECEIVED_TIMESPEC_STRUCT.size * 3) if CMSG_SPACE_available else 0
4848
)
4949

5050

@@ -556,11 +556,24 @@ def capture_message(
556556
# Fetching the timestamp
557557
assert len(ancillary_data) == 1, "only requested a single extra field"
558558
cmsg_level, cmsg_type, cmsg_data = ancillary_data[0]
559-
assert (
560-
cmsg_level == socket.SOL_SOCKET and cmsg_type == constants.SO_TIMESTAMPNS
559+
assert cmsg_level == socket.SOL_SOCKET and cmsg_type in (
560+
constants.SO_TIMESTAMPNS,
561+
constants.SO_TIMESTAMPING,
561562
), "received control message type that was not requested"
562563
# see https://man7.org/linux/man-pages/man3/timespec.3.html -> struct timespec for details
563-
seconds, nanoseconds = RECEIVED_TIMESTAMP_STRUCT.unpack_from(cmsg_data)
564+
if cmsg_type == constants.SO_TIMESTAMPNS:
565+
seconds, nanoseconds = RECEIVED_TIMESPEC_STRUCT.unpack_from(cmsg_data)
566+
else:
567+
# cmsg_type == constants.SO_TIMESTAMPING
568+
#
569+
# stamp[0] is the software timestamp
570+
# stamp[1] is deprecated
571+
# stamp[2] is the raw hardware timestamp
572+
offset = struct.calcsize(RECEIVED_TIMESPEC_STRUCT.format) * 2
573+
seconds, nanoseconds = RECEIVED_TIMESPEC_STRUCT.unpack_from(
574+
cmsg_data, offset=offset
575+
)
576+
564577
if nanoseconds >= 1e9:
565578
raise can.CanOperationError(
566579
f"Timestamp nanoseconds field was out of range: {nanoseconds} not less than 1e9"
@@ -619,6 +632,7 @@ def __init__(
619632
self,
620633
channel: str = "",
621634
receive_own_messages: bool = False,
635+
can_hardware_timestamps: bool = False,
622636
local_loopback: bool = True,
623637
fd: bool = False,
624638
can_filters: Optional[CanFilters] = None,
@@ -642,6 +656,17 @@ def __init__(
642656
channel using :attr:`can.Message.channel`.
643657
:param receive_own_messages:
644658
If transmitted messages should also be received by this bus.
659+
:param bool can_hardware_timestamps:
660+
Use raw hardware timestamp for can messages if available instead
661+
of the system timestamp. By default we use the SO_TIMESTAMPNS
662+
interface which provides ns resolution but low accuracy. If your
663+
can hardware supports it you can use this parameter to
664+
alternatively use the SO_TIMESTAMPING interface and request raw
665+
hardware timestamps. These are much higher precision but will
666+
almost certainly not be referenced to the time of day. There
667+
may be other pitfalls to such as loopback packets reporting with
668+
no timestamp at all.
669+
See https://www.kernel.org/doc/html/latest/networking/timestamping.html
645670
:param local_loopback:
646671
If local loopback should be enabled on this bus.
647672
Please note that local loopback does not mean that messages sent
@@ -659,6 +684,7 @@ def __init__(
659684
self.socket = create_socket()
660685
self.channel = channel
661686
self.channel_info = f"socketcan channel '{channel}'"
687+
self._can_hardware_timestamps = can_hardware_timestamps
662688
self._bcm_sockets: Dict[str, socket.socket] = {}
663689
self._is_filtered = False
664690
self._task_id = 0
@@ -703,12 +729,25 @@ def __init__(
703729
except OSError as error:
704730
log.error("Could not enable error frames (%s)", error)
705731

706-
# enable nanosecond resolution timestamping
707-
# we can always do this since
708-
# 1) it is guaranteed to be at least as precise as without
709-
# 2) it is available since Linux 2.6.22, and CAN support was only added afterward
710-
# so this is always supported by the kernel
711-
self.socket.setsockopt(socket.SOL_SOCKET, constants.SO_TIMESTAMPNS, 1)
732+
if not self._can_hardware_timestamps:
733+
# Utilise SO_TIMESTAMPNS interface :
734+
# we can always do this since
735+
# 1) it is guaranteed to be at least as precise as without
736+
# 2) it is available since Linux 2.6.22, and CAN support was only added afterward
737+
# so this is always supported by the kernel
738+
self.socket.setsockopt(socket.SOL_SOCKET, constants.SO_TIMESTAMPNS, 1)
739+
else:
740+
# Utilise SO_TIMESTAMPING interface :
741+
# Allows us to use raw hardware timestamps where available
742+
timestamping_flags = (
743+
constants.SOF_TIMESTAMPING_SOFTWARE
744+
| constants.SOF_TIMESTAMPING_RX_SOFTWARE
745+
| constants.SOF_TIMESTAMPING_RAW_HARDWARE
746+
)
747+
748+
self.socket.setsockopt(
749+
socket.SOL_SOCKET, constants.SO_TIMESTAMPING, timestamping_flags
750+
)
712751

713752
try:
714753
bind_socket(self.socket, channel)

can/logger.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ def _create_base_argument_parser(parser: argparse.ArgumentParser) -> None:
4646
choices=sorted(can.VALID_INTERFACES),
4747
)
4848

49+
parser.add_argument(
50+
"-H",
51+
"--hardwarets",
52+
help="Read hardware timestamps instead of system timestamps.",
53+
action="store_true",
54+
)
55+
4956
parser.add_argument(
5057
"-b", "--bitrate", type=int, help="Bitrate to use for the CAN bus."
5158
)
@@ -109,6 +116,8 @@ def _create_bus(parsed_args: argparse.Namespace, **kwargs: Any) -> can.BusABC:
109116
config["data_bitrate"] = parsed_args.data_bitrate
110117
if getattr(parsed_args, "can_filters", None):
111118
config["can_filters"] = parsed_args.can_filters
119+
if parsed_args.hardwarets:
120+
config["can_hardware_timestamps"] = True
112121

113122
return Bus(parsed_args.channel, **config)
114123

0 commit comments

Comments
 (0)