Link Layer Frame Send Scheduler, Internal RX Polling, Framework for Generic Frames + Fragmentation

This commit is contained in:
Justin Chow
2025-12-14 02:51:33 -05:00
committed by Johnathon Slightham
parent 0e29470be1
commit 92ddc3faf9
24 changed files with 3735 additions and 818 deletions

View File

@@ -11,11 +11,14 @@
#include "Frames.h"
#include "Tables.h"
#include "RMTManager.h"
#include <unordered_map>
#include "Scheduler.h"
#define DEBUG_LINK_TAG "LinkLayer"
#define CRC_POLYNOMIAL 0x1021
//look up table for crc
static const uint16_t crc16_table[256] = {
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF,
0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE,
@@ -33,8 +36,15 @@ static const uint16_t crc16_table[256] = {
0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92,
0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1,
0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0
}; //look up table for crc
};
#define ASYNC_QUEUE_WAIT_TICKS 100
/**
* @brief Class to represent the Data Link Layer
*
* @author Justin Chow
*/
class DataLinkManager{
public:
DataLinkManager(uint8_t board_id, uint8_t num_channels);
@@ -42,37 +52,36 @@ class DataLinkManager{
esp_err_t send(uint8_t dest_board, uint8_t* data, uint16_t data_len, FrameType type, uint8_t flag);
esp_err_t start_receive_frames(uint8_t curr_channel);
esp_err_t receive(uint8_t* data, size_t data_len, size_t* recv_len, uint8_t curr_channel);
esp_err_t print_frame_info(uint8_t* data, size_t data_len, uint8_t* message);
esp_err_t get_network_toplogy(RIPRow_public_matrix* matrix, size_t* matrix_size);
esp_err_t print_frame_info(uint8_t* data, size_t data_len, uint8_t* message, size_t message_len);
esp_err_t get_routing_table(RIPRow_public* table, size_t* table_size);
esp_err_t async_receive_info(uint16_t* frame_size, FrameHeader* header, uint8_t channel);
esp_err_t async_receive(uint8_t* data, uint16_t data_len, FrameHeader* header, uint8_t channel);
private:
uint8_t this_board_id = 0;
uint8_t num_channels = MAX_CHANNELS;
//std::priority_queue<Frame, std::vector<Frame>, FrameCompare> frame_queue; //create a priority queue - not in use
std::unique_ptr<RMTManager> phys_comms;
std::unordered_map<uint8_t, uint16_t> sequence_num_map;
volatile bool stop_tasks = false; //used by the tasks to know when to stop (set true when DataLinkManager is destroyed)
TaskHandle_t rip_broadcast_task = NULL;
TaskHandle_t rip_ttl_task = NULL;
esp_err_t set_board_id(uint8_t board_id);
esp_err_t get_board_id(uint8_t& board_id);
void print_binary(uint8_t byte);
void print_buffer_binary(const uint8_t* buffer, size_t length);
esp_err_t get_data_from_frame(uint8_t* data, size_t data_len, uint8_t* message, size_t* message_size, frame_header* header);
esp_err_t get_data_from_frame(uint8_t* data, size_t data_len, uint8_t* message, size_t* message_size, FrameHeader* header);
esp_err_t geneate_crc_16(uint8_t* data, size_t data_len, uint16_t* crc);
esp_err_t create_control_frame(uint8_t* data, uint16_t data_len, ControlFrame control_frame, uint8_t* send_data, size_t* send_data_len);
esp_err_t create_generic_frame(uint8_t* data, uint16_t data_len, GenericFrame generic_frame, uint16_t offset, uint8_t* send_data, size_t* send_data_len);
//==== RIP related functions ====
/**
* TODO for RIP:
* Periodic Routing Updates via timer (broadcast routing table every 30 seconds)
* Handle RIP table updates when a RIP table arrives (ensure there are no loops between boards of sending tables back and forth)
* TTL handling and route expiration
* TTLs in all rows should decrement at once via timer
*/
void init_rip();
esp_err_t rip_find_entry(uint8_t board_id, RIPRow** entry, bool reserve_row);
esp_err_t rip_update_entry(uint8_t new_hop, uint8_t channel, RIPRow** entry);
esp_err_t rip_add_entry(uint8_t board_id, uint8_t hops, uint8_t channel, RIPRow** entry);
esp_err_t rip_reset_entry_ttl(uint8_t board_id);
esp_err_t rip_get_row(RIPRow** entry, uint8_t row_num);
//this is stored locally with metadata `ttl`
// std::unordered_map<uint8_t, RIPRow> rip_table; //using a hash map to store the routes to other boards - will be used as we scale up
@@ -87,6 +96,95 @@ class DataLinkManager{
QueueHandle_t discovery_tables;
esp_err_t route_frame(uint8_t dest_id, uint8_t* channel_to_send);
//==== Frame Scheduling related functions ====
/**
* @brief Priority queue for each channel to schedule when to send frames
*
*/
std::priority_queue<SchedulerMetadata, std::vector<SchedulerMetadata>, FrameCompare> frame_queue[MAX_CHANNELS];
SemaphoreHandle_t sq_handle[MAX_CHANNELS];
void init_scheduler();
esp_err_t push_frame_to_scheduler(SchedulerMetadata frame, uint8_t channel);
TaskHandle_t scheduler_task = NULL;
/**
* @brief Schedules which frame to send
*
* Scheduler:
* - All frames will be pushed to the back onto a queue
* - When a generic frame sends a chunk, it will be pushed back to the queue for the next chunk to be sent
*
* Scheduling may change (above scheduler will lead to starvation of control frames depending on the number of generic frames/fragments to send)
*/
[[noreturn]] static void frame_scheduler(void* args);
/**
* @brief Scheduler sending the actual frame at the top of the heap on a channel
*
* @return esp_err_t
*/
esp_err_t scheduler_send(uint8_t channel);
//Generic Frame Receive Fragments
/**
* @brief Store a fragment that has been received
*
* @param fragment
* @param channel
* @return esp_err_t
*/
esp_err_t store_fragment(GenericFrame* fragment, uint8_t channel);
/**
* @brief Stores generic frame fragments
*
* Mapping:
* Board ID -> Sequence number -> Array of Generic Frame Fragments, with size of the number of expected fragments
*
* TODO:
* - When receiving a fragment, insert it into the map
* - When all fragments have been received in a sequence, remove the entire entry (sequence) from the map, and push final data for async receive
* - Sliding window + ACKs
*
*/
std::unordered_map<uint16_t, std::unordered_map<uint16_t, FragmentMetadata>> fragment_map;
esp_err_t complete_fragment(uint16_t board_id, uint16_t sequence_num, uint8_t channel);
SemaphoreHandle_t async_rx_queue_mutex[MAX_CHANNELS];
//Async receive
/**
* @brief Queue to store complete received frame data
*
* TODO:
* - Replace the public `receive()` with the `async_receive()`.
*
*/
std::queue<Rx_Metadata> async_receive_queue[MAX_CHANNELS];
esp_err_t start_receive_frames_rmt(uint8_t curr_channel);
/**
* @brief Receive thread entry point
*
* @param args
*/
[[noreturn]] static void receive_thread_main(void* args);
/**
* @brief Receive bytes from Physical Layer (RMT)
*
* @note This replaces the deprecated `receive` function
*
* @param channel Physical channel pair to look at
* @return esp_err_t
*/
esp_err_t receive_rmt(uint8_t channel);
TaskHandle_t receive_task = NULL;
};
#endif //DATA_LINK

View File

@@ -1,13 +1,21 @@
#ifdef DATA_LINK
#pragma once
#include "freertos/FreeRTOS.h"
#include <variant>
#include <cstdint>
#include <vector>
#define BROADCAST_ADDR 0xFF //used for discovery (finding the board's neighbours). this will mean the board ids will have 2^8-2 = 254 unique IDs that could be assigned
#define PC_ADDR 0x0 //setting 0 to be the PC
#define START_OF_FRAME 0xAB //0b1010_1011 - denotes the start of frame
#define MAX_GENERIC_DATA_LEN (1 << 16) //Max 65.5KiB
#define MAX_CONTROL_DATA_LEN (1 << 8) // Max 256B
#define MAX_GENERIC_NUM_FRAG (1 << 16) // Max 2**16 Fragments can be made with a generic frame (total 2**16 *MAX_CONTROL_DATA_LEN B of data can be sent ~ 16 MiB)
#define MAX_FRAME_QUEUE_SIZE 15 //Size of the queue for the frame scheduler (per channel)
//Flags
#define FLAG_FRAG 0x8 //0b1000 //this fragmented frame is part of a larger frame
#define FLAG_DISCOVERY 0x4 //0b0100
@@ -20,7 +28,7 @@
#define IS_CONTROL_FRAME(x) (((x) & 0x80) != 0)
#define CONTROL_FRAME_OVERHEAD 9
#define GENERIC_FRAME_OVERHEAD 12
#define GENERIC_FRAME_OVERHEAD 14
#define CONTROL_FRAME_TYPE 0x80 //if the frame type MSB is set to 1, use the control frame
//Types (total 2^4 = 16 different types)
@@ -45,7 +53,7 @@ typedef struct _control_frame{
uint16_t data_len; //Data Length (max 256B)
uint8_t data[MAX_CONTROL_DATA_LEN]; //Variable Length of Data
uint16_t crc_16; //CRC-16
} control_frame; //this will have a max size of 9 + 256B = 265B
} ControlFrame; //this will have a max size of 9 + 256B = 265B
typedef struct _data_link_frame{
uint8_t preamble; //Start of Frame
@@ -53,11 +61,12 @@ typedef struct _data_link_frame{
uint8_t receiver_id; //receiver board id
uint16_t seq_num; //sequence number to differentiate frames being sent from sender to receiver
uint8_t type_flag; //(type << 4) | flag - both are 4 bits
uint16_t frag_info; //(total_frag_num << 8) | frag_num - total_frag_num denotes the total number of fragmented frames to expect for this sequence number(?) and frag_num denotes the fragment frame num
uint16_t total_frag; //total number of fragments for this sequence
uint16_t frag_num; //current fragment number
uint16_t data_len; //Data Length (max 178B)
uint8_t data[MAX_GENERIC_DATA_LEN]; //Variable Length of Data
uint8_t data[MAX_CONTROL_DATA_LEN]; //Variable Length of Data
uint16_t crc_16; //CRC-16
} data_link_frame; //this will have a max size of ~65.5KiB
} GenericFrame; //this will have a max size of 14 + 2^8 B = 270 B
#pragma pack(pop)
typedef struct _header{
@@ -66,8 +75,26 @@ typedef struct _header{
uint8_t receiver_id; //receiver board id
uint16_t seq_num; //sequence number to differentiate frames being sent from sender to receiver
uint8_t type_flag; //(type << 4) | flag - both are 4 bits
uint16_t frag_info; //(total_frag_num << 8) | frag_num - total_frag_num denotes the total number of fragmented frames to expect for this sequence number(?) and frag_num denotes the fragment frame num
uint32_t frag_info; //(total_frag_num << 16) | frag_num - total_frag_num denotes the total number of fragmented frames to expect for this sequence number(?) and frag_num denotes the fragment frame num
uint16_t data_len; //Data Length (max 178B)
uint16_t crc_16; //CRC-16
} frame_header;
} FrameHeader;
using Frame = std::variant<ControlFrame, GenericFrame>;
ControlFrame make_control_frame_from_header(const FrameHeader& header);
GenericFrame make_generic_frame_from_header(const FrameHeader& header);
typedef struct _fragment_metadata {
std::vector<GenericFrame> fragments;
uint16_t num_fragments_rx;
} FragmentMetadata;
typedef struct _receive_metadata{
uint8_t* data;
uint16_t data_len;
FrameHeader header;
} Rx_Metadata;
#endif //DATA_LINK

View File

@@ -0,0 +1,67 @@
#ifdef DATA_LINK
#include "Frames.h"
#include "esp_timer.h"
#include <cstdint>
//TODO: need to make a private send() to accept a Frame type.
//the public send() is fine to keep but needs to be modified to simply create the Frame type based on the flags, data size, and args, and then push to the priority queue based on routing
//TODO: receive also needs to be updated, when receiving a frame not destined for that board; need to route that frame towards the actual destination (requires pushing that frame onto the priority queue)
//TODO: generic frames receive needs to be created (handle fragmenting, resend on corruption). ethe resend on corruption will be a lot of work as it requires ACK frames to be send and currently there is 0 support for ACK since we don't save any recently sent frames (can't resend)
#define SCHEDULER_MUTEX_WAIT 10 //max time duration to wait
#define SCHEDULER_PERIOD_MS 25
//Metadata representing the frame to be sent but is currently scheduled
typedef struct _frame_scheduler_metadata {
FrameHeader header; //header of the frame
uint16_t generic_frame_data_offset; //For data greater than MAX_CONTROL_DATA_LEN to keep track of fragment positions
int64_t enqueue_time_ns; //when the frame has been first enqueued into the priority queue
uint8_t* data; //dyanmically allocated memory - contains the actual data
uint16_t len; // length of the actual data
} SchedulerMetadata;
typedef struct _frame_compare {
/**
* @brief Uses aging based priority scheduling (linearly increasing priority with time)
*
* $P_f = B_f - A_f\alpha$
*
* - $P_f$ is the effective priority value (lower comes first)
*
* - $B_f$ is the base priority
*
* - $A_f$ is the age (amount of time the frame has waited in the queue)
*
* - $\alpha$ is the aging factor (rate at which a frame increases priority)
*
* @param a
* @param b
* @return true
* @return false
*/
bool operator()(const SchedulerMetadata& a, const SchedulerMetadata& b) const {
int64_t now = esp_timer_get_time();
double age_a = (now - a.enqueue_time_ns) / 1e6;
double age_b = (now - a.enqueue_time_ns) / 1e6;
// Base priorities: lower is higher priority
double base_a = (IS_CONTROL_FRAME(a.header.type_flag)) ? 0.0 : 10.0;
double base_b = (IS_CONTROL_FRAME(b.header.type_flag)) ? 0.0 : 10.0;
// Aging coefficient (tune this)
constexpr double aging_factor = 0.1;
double effective_a = base_a - age_a * aging_factor;
double effective_b = base_b - age_b * aging_factor;
// If effective priority equal, fall back to enqueue time (FIFO)
if (effective_a == effective_b) {
return a.enqueue_time_ns > b.enqueue_time_ns;
}
// Return true if a has *lower* priority (so b stays on top)
return effective_a < effective_b;
}
} FrameCompare;
#endif //DATA_LINK