[#102763] external-control: Add CAN support
diff --git a/src/Infrastructure b/src/Infrastructure index a02d8e0..a0fcf54 160000 --- a/src/Infrastructure +++ b/src/Infrastructure
@@ -1 +1 @@ -Subproject commit a02d8e0fbfff1f081ecc8cd0b2ff1d2f7d4bf2b6 +Subproject commit a0fcf54f9a31591e607e75143b796aeb4da626b8
diff --git a/src/Renode/Network/ExternalControl/CANBus.cs b/src/Renode/Network/ExternalControl/CANBus.cs new file mode 100644 index 0000000..1b33fc8 --- /dev/null +++ b/src/Renode/Network/ExternalControl/CANBus.cs
@@ -0,0 +1,106 @@ +// +// Copyright (c) 2010-2026 Antmicro +// +// This file is licensed under the MIT License. +// Full license text is available in 'licenses/MIT.txt'. +// +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; + +using Antmicro.Renode.Core; +using Antmicro.Renode.Core.CAN; +using Antmicro.Renode.Logging; +using Antmicro.Renode.Peripherals; +using Antmicro.Renode.Peripherals.CAN; +using Antmicro.Renode.Utilities; + +namespace Antmicro.Renode.Network.ExternalControl; + +public class CANBus(ExternalControlServer parent) : BaseCommand(parent), IInstanceBasedCommand<CANExternalControlBus>, IEmulationElement +{ + public static int MaximumCanFrameSize = 64; + + public MessagePayload Invoke(CANExternalControlBus instance, List<byte> data) + { + if(data.Count < 1) + { + return MessagePayload.Error(Identifier, $"Expected at least {1 + InstanceBasedCommandHeaderSize} bytes of payload"); + } + var command = (CANBusCommands)data[0]; + Logger.DebugLog(instance, "Received {0} command", command); + + switch(command) + { + case CANBusCommands.SendFrame: + this.DebugLog("Received SendFrameCommand with data: {0}", data.Select(x => x.ToString("02x")).Stringify()); + + var data_array = data.ToArray(); + var packet_length = Misc.ByteArrayRead(1, data_array); + var id = Misc.ByteArrayRead(5, data_array); + + if(data.Count != packet_length + 9) + { + return MessagePayload.Error(Identifier, $"Incorrect number of bytes in payload. Expected {packet_length + 9}"); + } + + var packet = data.Skip(9).ToArray(); + + instance.SendFrame(packet, id); + break; + + case CANBusCommands.RegisterCallbacks: + var ed = (int) Misc.ByteArrayRead(1, data.ToArray()); + Logger.DebugLog(this, "Attaching ReceivedMessage callback to instance '{0}'", instance.GetName()); + instance.ReceivedMessage += (frame) => ReceivedFrame(frame, ed); + break; + + default: + return MessagePayload.Error(Identifier, "Unexpected command format"); + } + return MessagePayload.Success(Identifier); + } + + public void ReceivedFrame(CANMessageFrame message, int callbackIndex) + { + var eventHeader = EventHeader.Create(EmulationManager.Instance.CurrentEmulation.MasterTimeSource.ElapsedVirtualTime.TotalNanoseconds, message); + + var response = parent.SendRequest(MessagePayload.Event(Identifier, callbackIndex, eventHeader, message.Data)); + LogOnErrorResponse(response); + } + + public override MessagePayload Invoke(List<byte> data) => this.InvokeHandledWithInstance(data); + + public override Command Identifier => Command.CANBus; + + public InstanceCollection<CANExternalControlBus> Instances { get; } = new InstanceCollection<CANExternalControlBus>(); + + private const int InstanceBasedCommandHeaderSize = IInstanceBasedCommandExtensions.HeaderSize; + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct EventHeader + { + public static EventHeader Create(ulong timestamp, CANMessageFrame message) + { + var len = message.Data.Length; + if(len > MaximumCanFrameSize) + { + throw new ArgumentOutOfRangeException(); + } + return new EventHeader() { TimestampNanoseconds = timestamp, Id = message.Id, PacketLength = len }; + } + + public ulong TimestampNanoseconds; + + public int PacketLength; + + public uint Id; + } + + public enum CANBusCommands : byte + { + SendFrame = 0, + RegisterCallbacks = 2, + } +}
diff --git a/src/Renode/Network/ExternalControl/ExternalControlServer.cs b/src/Renode/Network/ExternalControl/ExternalControlServer.cs index 6ec70ed..910603a 100644 --- a/src/Renode/Network/ExternalControl/ExternalControlServer.cs +++ b/src/Renode/Network/ExternalControl/ExternalControlServer.cs
@@ -141,6 +141,7 @@ commandHandlers.Register(new GPIOPort(this)); commandHandlers.Register(new SystemBus(this)); commandHandlers.Register(new CheckVersion(this)); + commandHandlers.Register(new CANBus(this)); var getMachineHandler = new GetMachine(this); Machines = getMachineHandler;
diff --git a/src/Renode/Network/ExternalControl/ICommand.cs b/src/Renode/Network/ExternalControl/ICommand.cs index d7843a7..95278e7 100644 --- a/src/Renode/Network/ExternalControl/ICommand.cs +++ b/src/Renode/Network/ExternalControl/ICommand.cs
@@ -24,6 +24,7 @@ SystemBus, TimeElapsedCallback, SPI, + CANBus, } public interface ICommand
diff --git a/src/Renode/Network/ExternalControl/MessagePayload.cs b/src/Renode/Network/ExternalControl/MessagePayload.cs index 772342a..48e461b 100644 --- a/src/Renode/Network/ExternalControl/MessagePayload.cs +++ b/src/Renode/Network/ExternalControl/MessagePayload.cs
@@ -51,6 +51,14 @@ return new MessagePayload(command, CommandType.EventRequest, BitConverter.GetBytes(eventId).Concat(payload.AsRawBytes()).ToArray()); } + public static MessagePayload Event<T>(Command command, int eventId, T payloadHeader, byte[] data) where T : struct + { + return new MessagePayload(command, CommandType.EventRequest, + BitConverter.GetBytes(eventId) + .Concat(payloadHeader.AsRawBytes()) + .Concat(data).ToArray()); + } + public MessagePayload(Command cmd, CommandType type, byte[] data) { Command = cmd;
diff --git a/tests/external-control/external-control.robot b/tests/external-control/external-control.robot index 8177dd8..533e2ea 100644 --- a/tests/external-control/external-control.robot +++ b/tests/external-control/external-control.robot
@@ -6,6 +6,7 @@ *** Variables *** ${EXTERNAL_CONTROL_DIR} ${CURDIR}/../../tools/external_control_client +${CREATE_SNAPSHOT_ON_FAIL} False # This will be overriden by the test setup ${BUILD_DIR} ${EXTERNAL_CONTROL_DIR}/build ${PORT} 3344 @@ -335,3 +336,26 @@ END Terminate Process ${proc} + +Should Run CAN Sample + [Tags] exclude_windows + Set Test Variable ${sender_id} 0x55 + Set Test Variable ${receiver_id} 0xAA + Set Test Variable ${msg_hex} C001D00D + + Execute Command mach create "machine" + Execute Command machine LoadPlatformDescriptionFromString "external_client_bus: CAN.CANExternalControlBus @ sysbus" + Execute Command emulation CreateCANHub "canHub" + Execute Command connector Connect external_client_bus canHub + + Create Can Tester canHub + + Build Sample can + + ${proc}= Start Sample can ${PORT} machine external_client_bus + + Send ISOTP Message ${sender_id} ${receiver_id} ${msg_hex} + ${received_msg}= Wait For Frame With Id 0x55 timeout=10000 + ${received_hex}= Evaluate $received_msg.hex().upper() + Should Contain ${received_hex} ${msg_hex} +
diff --git a/tools/external_control_client/examples/can/main.c b/tools/external_control_client/examples/can/main.c new file mode 100644 index 0000000..5cc6877 --- /dev/null +++ b/tools/external_control_client/examples/can/main.c
@@ -0,0 +1,157 @@ +// +// Copyright (c) 2010-2026 Antmicro +// +// This file is licensed under MIT License. +// Full license text is available in 'licenses/MIT.txt' file. +// +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <inttypes.h> +#include <unistd.h> +#include <stdlib.h> +#include <pthread.h> + +#include "renode_api.h" + +uint8_t next_packet_to_send[MAX_CAN_FRAME_SIZE]; +int next_packet_length; +uint32_t next_packet_id; +pthread_mutex_t next_packet_lock; + +static void exit_with_usage_info(const char *argv0) +{ + fprintf(stderr, + "Usage:\n" + " %s <PORT> <MACHINE_NAME> <CAN_NAME>\n", + argv0); + exit(EXIT_FAILURE); +} + +static char *get_error_message(renode_error_t *error) +{ + if (error->message == NULL) + { + return "<no message>"; + } + return error->message; +} + +static int try_renode_disconnect(renode_t **renode) +{ + renode_error_t *error; + if ((error = renode_disconnect(renode)) != NO_ERROR) { + fprintf(stderr, "Disconnecting from Renode failed with: %s\n", get_error_message(error)); + return -1; + } + return 0; +} + +typedef struct { + char *machine_name; + char *can_name; +} can_event_user_data; + +void can_callback(void *user_data, renode_can_event_data_t *event_data) +{ + + can_event_user_data *udata = (can_event_user_data *)user_data; + printf("%s:%s can packet with length %d received:\n\t", udata->machine_name, udata->can_name, event_data->packet_length); + + + printf("0x"); + for (int current_position = 0; current_position < event_data->packet_length; current_position++) { + printf("%02x", event_data->packet[current_position]); + } + printf("\n"); + fflush(stdout); + + pthread_mutex_lock(&next_packet_lock); + memcpy(&next_packet_to_send, &event_data->packet, event_data->packet_length); + next_packet_id = event_data->packet_id; + next_packet_length = event_data->packet_length; + pthread_mutex_unlock(&next_packet_lock); +} + +int main(int argc, char **argv) +{ + if (argc < 4) { + exit_with_usage_info(argv[0]); + } + char *port_number_string = argv[1]; + char *machine_name = argv[2]; + char *can_name = argv[3]; + + pthread_mutex_init(&next_packet_lock, NULL); + + // get Renode, machine and can instances + renode_error_t *error; + renode_t *renode; + if ((error = renode_connect(port_number_string, &renode)) != NO_ERROR) { + fprintf(stderr, "Connecting to Renode failed with: %s\n", get_error_message(error)); + goto fail; + } + + renode_machine_t *machine; + if ((error = renode_get_machine(renode, machine_name, &machine)) != NO_ERROR) { + fprintf(stderr, "Getting '%s' machine object failed with: %s\n", machine_name, get_error_message(error)); + goto fail_renode; + } + + renode_can_t *can; + if ((error = renode_get_can(machine, can_name, &can)) != NO_ERROR) { + fprintf(stderr, "Getting '%s' CAN object failed with: %s\n", can_name, get_error_message(error)); + goto fail_machine; + } + + can_event_user_data user_data = (can_event_user_data){ + .machine_name = machine_name, + .can_name = can_name, + }; + + if ((error = renode_register_can_callback(can, &user_data, can_callback)) != NO_ERROR) { + fprintf(stderr, "Registering CAN event for peripheral '%s' failed with: '%s'\n", can_name, get_error_message(error)); + goto fail_can; + } + + while(true){ + + pthread_mutex_lock(&next_packet_lock); + if (next_packet_length > 0) + { + + printf("About to send can message"); + fflush(stdout); + if((error = renode_send_can_message(can, &next_packet_to_send, next_packet_length, next_packet_id))) { + fprintf(stderr, "send can message failed with: %s\n", get_error_message(error)); + goto fail_can; + } + next_packet_length = 0; + } + pthread_mutex_unlock(&next_packet_lock); + } + // clean up + + free(can); + free(machine); + if (try_renode_disconnect(&renode)) { + exit(EXIT_FAILURE); + } + + fflush(stdout); + exit(EXIT_SUCCESS); + +fail_can: + free(can); +fail_machine: + free(machine); +fail_renode: + try_renode_disconnect(&renode); + free(renode); +fail: + renode_free_error(error); + fflush(stdout); + fflush(stderr); + exit(EXIT_FAILURE); +}
diff --git a/tools/external_control_client/include/renode_api.h b/tools/external_control_client/include/renode_api.h index f7c9f16..0e5e645 100644 --- a/tools/external_control_client/include/renode_api.h +++ b/tools/external_control_client/include/renode_api.h
@@ -83,6 +83,13 @@ typedef struct renode_gpio renode_gpio_t; /** + * @brief Renode CAN bus API handle + * + * @copydetails renode_t + */ +typedef struct renode_can renode_can_t; + +/** * @brief Renode bus manager API handle * * @copydetails renode_t @@ -358,6 +365,54 @@ renode_error_t *renode_register_gpio_state_change_callback(renode_gpio_t *gpio, int32_t id, void *user_data, void (*callback)(void *, renode_gpio_event_data_t *)); +/* CAN */ + +#define MAX_CAN_FRAME_SIZE 64 + +/** + * CAN bus event data + */ +typedef struct { + renode_time_t time; + int32_t packet_length; + uint32_t packet_id; + uint8_t packet[]; +} renode_can_event_data_t; + +/** + * @brief Function preparing CAN handle + * + * @note Handle's internal memory is dynamically allocated so `*can` should be freed when it's no longer used. + * + * @param[in] machine machine handle + * @param[in] name CANBus name + * @param[out] can handle associated with the requested CAN instance + * @return a pointer to error structure if error occurred, otherwise NULL + */ +renode_error_t *renode_get_can(renode_machine_t *machine, const char *name, renode_can_t **can); + +/** + * @brief Function registering callback when CAN message is received + * + * @param[in] can CAN instance handle + * @param[in] user_data pointer to data passed to the callback when it's invoked + * @param[in] callback callback to be invoked when message is received + * @return a pointer to error structure if error occurred, otherwise NULL + */ +renode_error_t *renode_register_can_callback(renode_can_t *can, void *user_data, void (*callback)(void *, renode_can_event_data_t *)); + +/** + * @brief Function sending CAN message + * + * @param[in] can CAN instance handle + * @param[in] packet pointer to fixed size, 64 byte can packet array + * @param[in] packet_length length of send packet + * @param[in] packet_id packet_id + * @return a pointer to error structure if error occurred, otherwise NULL + */ +renode_error_t *renode_send_can_message(renode_can_t *can, void *packet, int packet_length, uint32_t packet_id); + + /* System bus */ /**
diff --git a/tools/external_control_client/lib/common.h b/tools/external_control_client/lib/common.h index ef4fc35..b4e6dff 100644 --- a/tools/external_control_client/lib/common.h +++ b/tools/external_control_client/lib/common.h
@@ -40,6 +40,7 @@ SYSTEM_BUS, TIME_ELAPSED_CALLBACK, SPI, + CAN_BUS, } api_command_t; // Needs to be in sync with `Antmicro.Renode.Network.ExternalControl.CommandType` in C#
diff --git a/tools/external_control_client/lib/renode_api.c b/tools/external_control_client/lib/renode_api.c index 2800aba..eec654b 100644 --- a/tools/external_control_client/lib/renode_api.c +++ b/tools/external_control_client/lib/renode_api.c
@@ -4,6 +4,7 @@ // This file is licensed under MIT License. // Full license text is available in 'licenses/MIT.txt' file. // +#include "renode_api.h" #include <fcntl.h> #include <inttypes.h> #include <netdb.h> @@ -39,6 +40,11 @@ int32_t id; }; +struct renode_can { + renode_machine_t *machine; + int32_t id; +}; + struct renode_bus_context { renode_machine_t *machine; int32_t id; @@ -144,7 +150,6 @@ #define MAX_CALLBACK_COUNT 1024 typedef void (*raw_callback_t)(void *, void *); - static raw_callback_t callbacks[MAX_CALLBACK_COUNT]; static void *callback_user_data[MAX_CALLBACK_COUNT]; static int32_t callbacks_count; @@ -181,6 +186,19 @@ callbacks[ed](callback_user_data[ed], &event_data); return renode_connection_send_message(conn, id, RESPONSE_HEADER(cmd, TYPE_SUCCESS)); + case CAN_BUS: + assert_response(size >= sizeof(renode_can_event_data_t), ERRMSG_UNEXPECTED_RESPONSE_PAYLOAD_SIZE); + assert_response(size <= sizeof(renode_can_event_data_t) + MAX_CAN_FRAME_SIZE, ERRMSG_UNEXPECTED_RESPONSE_PAYLOAD_SIZE); + + renode_can_event_data_t *can_data; + can_data = xmalloc(size); + memcpy(can_data, data, size); + + callbacks[ed](callback_user_data[ed], can_data); + assert_response(size == sizeof(renode_can_event_data_t) + can_data->packet_length, ERRMSG_UNEXPECTED_RESPONSE_PAYLOAD_SIZE); + renode_error_t *result = renode_connection_send_message(conn, id, RESPONSE_HEADER(cmd, TYPE_SUCCESS)); + free(can_data); + return result; case TIME_ELAPSED_CALLBACK: assert_response(size == sizeof(renode_time_t), ERRMSG_UNEXPECTED_RESPONSE_PAYLOAD_SIZE); @@ -571,6 +589,75 @@ ); } +typedef enum +{ + SEND_FRAME = 0, + RECEIVED_FRAME = 1, + REGISTER_CALLBACKS = 2, +} canbus_operation_t; + +typedef struct { + struct __attribute__((packed)) { + int32_t id; + int8_t command; + } header; + int32_t packet_length; + uint32_t id; + void * packet; +} canbus_command_t; + +renode_error_t *renode_get_can(renode_machine_t *machine, const char *name, renode_can_t **can) +{ + int32_t id; + return_error_if_fails(renode_get_instance_descriptor(machine, CAN_BUS, name, &id)); + + *can = xmalloc(sizeof(renode_can_t)); + (*can)->machine = machine; + (*can)->id = id; + + return NO_ERROR; +} + +renode_error_t *renode_send_can_message(renode_can_t *can, void *packet, int packet_length, uint32_t packet_id) +{ + canbus_command_t cmd = { + .header = { + .id = can->id, + .command = SEND_FRAME, + }, + .packet_length = packet_length, + .id = packet_id, + }; + + return_error_if_fails(renode_connection_send_request(can->machine->renode->conn, generic_response_handler, &cmd, + REQUEST_HEADER(CAN_BUS), + {&cmd.header, sizeof(cmd.header)}, + {&cmd.packet_length, sizeof(cmd.packet_length)}, + {&cmd.id, sizeof(cmd.id)}, + {packet, packet_length}, + )); + + return NO_ERROR; +} + +renode_error_t *renode_register_can_callback(renode_can_t *can, void *user_data, void (*callback)(void *, renode_can_event_data_t *)) +{ + int32_t ed; + return_error_if_fails(register_callback((raw_callback_t)callback, user_data, &ed)); + + canbus_command_t cmd = { + .header = { + .command = REGISTER_CALLBACKS, + }, + }; + + return renode_connection_send_request(can->machine->renode->conn, generic_response_handler, &cmd, + REQUEST_HEADER(CAN_BUS), + {&cmd.header, sizeof(cmd.header)}, + {&ed, sizeof(ed)}, + ); +} + renode_error_t *renode_get_bus_context(renode_machine_t *machine, const char *name, renode_bus_context_t **ctx) { int32_t id; @@ -814,6 +901,8 @@ return NO_ERROR; // This command doesn't return any data, we only care about a successful status case SPI: return NO_ERROR; // This command doesn't return any data, we only care about a successful status + case CAN_BUS: + return NO_ERROR; // This command doesn't return any data, we only care about a successful status default: return create_fatal_error_static("Encountered a command without a response handler"); }