Skip to content

Commit 84e002c

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 84e002c

File tree

3 files changed

+57
-11
lines changed

3 files changed

+57
-11
lines changed

can/interfaces/socketcan/constants.py

+4
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_SOFTWARE = 1 << 4
9+
SOF_TIMESTAMPING_RX_SOFTWARE = 1 << 3
10+
SOF_TIMESTAMPING_RAW_HARDWARE = 1 << 6
711

812
CAN_ERR_FLAG = 0x20000000
913
CAN_RTR_FLAG = 0x40000000

can/interfaces/socketcan/socketcan.py

+44-11
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,26 @@ 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+
# See chapter 2.1.2 Receive timestamps in
573+
# linux/Documentation/networking/timestamping.txt
574+
offset = struct.calcsize(RECEIVED_TIMESPEC_STRUCT.format) * 2
575+
seconds, nanoseconds = RECEIVED_TIMESPEC_STRUCT.unpack_from(
576+
cmsg_data, offset=offset
577+
)
578+
564579
if nanoseconds >= 1e9:
565580
raise can.CanOperationError(
566581
f"Timestamp nanoseconds field was out of range: {nanoseconds} not less than 1e9"
@@ -619,6 +634,7 @@ def __init__(
619634
self,
620635
channel: str = "",
621636
receive_own_messages: bool = False,
637+
can_hardware_timestamps: bool = False,
622638
local_loopback: bool = True,
623639
fd: bool = False,
624640
can_filters: Optional[CanFilters] = None,
@@ -642,6 +658,9 @@ def __init__(
642658
channel using :attr:`can.Message.channel`.
643659
:param receive_own_messages:
644660
If transmitted messages should also be received by this bus.
661+
:param bool can_hardware_timestamps:
662+
Use hardware timestamp for can messages if available instead of
663+
the system timestamp.
645664
:param local_loopback:
646665
If local loopback should be enabled on this bus.
647666
Please note that local loopback does not mean that messages sent
@@ -659,6 +678,7 @@ def __init__(
659678
self.socket = create_socket()
660679
self.channel = channel
661680
self.channel_info = f"socketcan channel '{channel}'"
681+
self.can_hardware_timestamps = can_hardware_timestamps
662682
self._bcm_sockets: Dict[str, socket.socket] = {}
663683
self._is_filtered = False
664684
self._task_id = 0
@@ -703,12 +723,25 @@ def __init__(
703723
except OSError as error:
704724
log.error("Could not enable error frames (%s)", error)
705725

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)
726+
if not self.can_hardware_timestamps:
727+
# Utilise SOF_TIMESTAMPNS interface :
728+
# we can always do this since
729+
# 1) it is guaranteed to be at least as precise as without
730+
# 2) it is available since Linux 2.6.22, and CAN support was only added afterward
731+
# so this is always supported by the kernel
732+
self.socket.setsockopt(socket.SOL_SOCKET, constants.SO_TIMESTAMPNS, 1)
733+
else:
734+
# Utilise SOF_TIMESTAMPNS interface :
735+
# Allows us to use hardware timestamps where available
736+
timestamping_flags = (
737+
constants.SOF_TIMESTAMPING_SOFTWARE
738+
| constants.SOF_TIMESTAMPING_RX_SOFTWARE
739+
| constants.SOF_TIMESTAMPING_RAW_HARDWARE
740+
)
741+
742+
self.socket.setsockopt(
743+
socket.SOL_SOCKET, constants.SO_TIMESTAMPING, timestamping_flags
744+
)
712745

713746
try:
714747
bind_socket(self.socket, channel)

can/logger.py

+9
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)