ether2ser 0.1.0
Ethernet <-> synchronous V.24 bridge firmware for RP2040 + W5500
Loading...
Searching...
No Matches
event_loop.c
Go to the documentation of this file.
1/*
2 * ether2ser - Ethernet <-> synchronous V.24 (RS-232/V.28) bridge
3 *
4 * File: src/system/event_loop.c
5 * Purpose: Main polling loop driving data path and control-plane dispatch.
6 *
7 * SPDX-License-Identifier: Apache-2.0
8 *
9 * Copyright (c) 2026 Florian <f.leuze@outlook.de>
10 */
11
12// Related headers
13#include "system/event_loop.h"
14
15// Standard library headers
16#include <inttypes.h>
17#include <stdbool.h>
18#include <stdint.h>
19#include <stdio.h>
20#include <string.h>
21
22// Library Headers
23#include "hardware/watchdog.h"
24#include "pico/time.h"
25#include "pico/types.h"
26
27// Project Headers
29#include "drivers/tx_queue.h"
33#include "protocol/hdlc_sync.h"
34#include "system/app_context.h"
35#include "system/cli_usb_cdc.h"
36#include "system/common.h"
37#include "system/error.h"
39#include "system/event_queue.h"
40// Generated headers
41
42#define MAIN_LOOP_SLEEP_MS 1
43#define MAIN_LOOP_SLEEP_US 50
44
45#define TX_QUEUE_DRAIN_CHUNK_SIZE 32U
46#define TX_QUEUE_HIGH_WM 48U
47#define TX_QUEUE_LOW_WM 12U
48
49#define EVENT_LOOP_MAX_EVENTS_AT_ONCE 20
50#define EVENT_LOOP_MAX_EVENTS_WHILE_TX_ACTIVE 2
51#define HDLC_DECODE_FAIL_STREAK_LIMIT 4U
52#define HDLC_SYNC_IDLE_TIMEOUT_US 20000U
53#define HDLC_SYNC_NO_PROGRESS_MAX_BYTES 2048U
54#define HDLC_SYNC_NO_PROGRESS_MAX_BYTES_EXTERNAL 12288
55
56static void print_prompt(app_ctx_t* app)
57{
58 if (app->need_prompt)
59 {
60 LOG_PLAIN("ether2ser> ");
61 app->need_prompt = false;
62 }
63}
64
65static bool event_loop_should_drop_hunt_idle_byte(app_ctx_t* app, uint8_t rx_byte,
66 uint32_t* idle_run_length)
67{
68 if (!app || !idle_run_length)
69 {
70 return false;
71 }
72
73 bool in_external_hunting =
75 if (!in_external_hunting)
76 {
77 *idle_run_length = 0U;
78 return false;
79 }
80
81 if (rx_byte == 0x00U || rx_byte == 0xFFU)
82 {
83 if (*idle_run_length >= 2U)
84 {
85 return true;
86 }
87 (*idle_run_length)++;
88 return false;
89 }
90
91 *idle_run_length = 0U;
92 return false;
93}
94
105
106static void drain_rx_until_empty(app_ctx_t* app, event_loop_runtime_t* runtime, size_t* rx_drained)
107{
108 while (rx_get(&runtime->rx_byte))
109 {
110 runtime->work_done = true;
111 (*rx_drained)++;
113 &runtime->hunt_idle_run_length))
114 {
116 continue;
117 }
118 if (!hdlc_sync_acc_process_byte(&app->accumulator, runtime->rx_byte))
119 {
121 }
122 }
123}
124
125static void update_rx_drain_stats(app_ctx_t* app, event_loop_runtime_t* event_loop_runtime,
126 const size_t rx_drained_early, const uint64_t now_us)
127{
128
129 app->stats.serial_rx_bytes += rx_drained_early;
131 {
133 }
134 if (rx_drained_early > 0U)
135 {
136 event_loop_runtime->last_rx_byte_us = now_us;
137 }
138}
139
140static void poll_and_enqueue_udp_rx(app_ctx_t* app, event_loop_runtime_t* event_loop_runtime)
141{
142
144 {
145 app->stats.udp_rx_frames++;
146 e2s_error_t enqueue_result =
148 if (enqueue_result == E2S_OK)
149 {
150 event_loop_runtime->work_done = true;
151 app->stats.hdlc_tx_frames++;
152 }
153 else
154 {
155 if (enqueue_result == E2S_ERR_TX_QUEUE_FULL)
156 {
158 }
159 LOG_ERROR("TX Queue Enqueue failed: %d.\r\n", enqueue_result);
160 }
161 memset(app->rx_frame_buffer.payload, 0, app->rx_frame_buffer.length);
162 app->rx_frame_buffer.length = 0;
163 }
164}
165
166static size_t poll_tx_pipeline(app_ctx_t* app, event_loop_runtime_t* event_loop_runtime)
167{
168
169 size_t bytes_drained = 0;
170 if (poll_queue_stats(&app->tx_queue) != E2S_OK)
171 {
172 LOG_ERROR("Poll Queue Stats failed.\r\n");
173 }
174
175 // Poll the tx queue. This writes out bytes on the serial line
176 if (tx_queue_drain(&app->tx_queue, TX_QUEUE_DRAIN_CHUNK_SIZE, &bytes_drained) != E2S_OK)
177 {
178 LOG_ERROR("Poll Queue Drain failed.\r\n");
179 }
180 else if (bytes_drained > 0U)
181 {
182 event_loop_runtime->work_done = true;
183 }
184
185 // If the tx queue is empty check if the fifo is empty to and reset rts
186 if (tx_queue_is_empty(&app->tx_queue))
187 {
188 // Currently we dont evaluate the result, but the api offers it
189 (void)tx_poll();
190 }
191 return bytes_drained;
192}
193
194static void poll_hdlc_idle_timeout(app_ctx_t* app, event_loop_runtime_t* event_loop_runtime,
195 const uint64_t now_us)
196{
197 bool frame_in_progress = (app->accumulator.state != HDLC_SYNC_STATE_HUNTING) ||
198 (app->accumulator.position > 0U) ||
199 (app->reconstructed_frame.length > 0U);
200 if (frame_in_progress && event_loop_runtime->last_rx_byte_us != 0U &&
201 (now_us - event_loop_runtime->last_rx_byte_us) > HDLC_SYNC_IDLE_TIMEOUT_US)
202 {
204 LOG_DEBUG("HDLC idle timeout resync\r\n");
206 event_loop_runtime->hdlc_decode_fail_streak = 0U;
208 app->reconstructed_frame.length = 0U;
209 }
210}
211
212static void decode_hdlc_to_udp_tx(app_ctx_t* app, event_loop_runtime_t* event_loop_runtime)
213{
214 event_loop_runtime->work_done = true;
215 app->stats.hdlc_frame_ready++;
216 event_loop_runtime->last_frame_ready_bytes = app->stats.serial_rx_bytes;
217 app->tx_frame_buffer.length = 0;
219 &app->tx_frame_buffer.length, true))
220 {
221 app->stats.hdlc_decode_ok++;
222 event_loop_runtime->hdlc_decode_fail_streak = 0;
224 app->stats.udp_tx_frames++;
226 }
227 else
228 {
229 app->stats.hdlc_decode_fail++;
230 if (app->stats.hdlc_decode_fail == 1U)
231 {
232 LOG_DEBUG("HDLC first decode fail: off=%u shift_right=%u cand_start=%zu "
233 "cand_end=%zu pos=%zu proc=%zu\r\n",
234 (unsigned)app->accumulator.bit_offset,
235 (unsigned)app->accumulator.align_shift_right,
238 }
240 if (event_loop_runtime->hdlc_decode_fail_streak < UINT8_MAX)
241 {
242 event_loop_runtime->hdlc_decode_fail_streak++;
243 }
245 {
247 LOG_DEBUG("HDLC hard resync after %u decode fails\r\n",
248 (unsigned)event_loop_runtime->hdlc_decode_fail_streak);
250 event_loop_runtime->hdlc_decode_fail_streak = 0;
251 }
252 }
255}
256
257static void drain_hdlc_frames_to_udp(app_ctx_t* app, event_loop_runtime_t* event_loop_runtime)
258{
259 while (true)
260 {
262 if (acc_result == E2S_ERR_HDLC_ACC_FRAME_READY)
263 {
264 decode_hdlc_to_udp_tx(app, event_loop_runtime);
265 continue;
266 }
267 if (acc_result != E2S_OK)
268 {
271 }
272 break;
273 }
274}
275static void poll_hdlc_no_progress(app_ctx_t* app, event_loop_runtime_t* event_loop_runtime)
276{
277 // External-clock mode continuously feeds idle bytes. In HUNTING this is normal and
278 // must not trigger no-progress resyncs, otherwise the next real frame gets clipped.
279 bool decode_in_progress = (app->accumulator.state != HDLC_SYNC_STATE_HUNTING) ||
281 (app->reconstructed_frame.length > 0U);
282 if (!decode_in_progress)
283 {
284 // In external-clock idle periods serial_rx_bytes keeps increasing.
285 // Reset baseline while idle so first frame after a gap is not
286 // immediately forced into no-progress resync.
287 event_loop_runtime->last_frame_ready_bytes = app->stats.serial_rx_bytes;
288 }
289 bool have_decode_lock = (app->stats.hdlc_decode_ok > 0U);
290 const uint64_t no_progress_max_bytes = app->v24_config.external_clock
293 if (have_decode_lock && decode_in_progress &&
294 (app->stats.serial_rx_bytes > event_loop_runtime->last_frame_ready_bytes) &&
295 ((app->stats.serial_rx_bytes - event_loop_runtime->last_frame_ready_bytes) >
296 no_progress_max_bytes))
297 {
299 LOG_DEBUG("HDLC no-progress resync after %" PRIu64 " bytes (limit=%" PRIu64 ")\r\n",
300 (app->stats.serial_rx_bytes - event_loop_runtime->last_frame_ready_bytes),
301 no_progress_max_bytes);
303 event_loop_runtime->hdlc_decode_fail_streak = 0U;
305 app->reconstructed_frame.length = 0U;
306 event_loop_runtime->last_frame_ready_bytes = app->stats.serial_rx_bytes;
307
309 }
310}
311
313{
319
320 hdlc_decode_stats_t decode_stats = {0};
321 hdlc_decode_stats_snapshot(&decode_stats);
323 app->stats.decode_fail_too_short = decode_stats.too_short;
326 app->stats.decode_fail_crc_mismatch = decode_stats.crc_mismatch;
327
328 size_t tx_queue_count = tx_queue_get_count(&app->tx_queue);
329 if (tx_queue_count > app->stats.tx_queue_used_max)
330 {
331 app->stats.tx_queue_used_max = tx_queue_count;
332 }
333 size_t event_queue_count = event_queue_get_count();
334 if (event_queue_count > app->stats.event_queue_used_max)
335 {
336 app->stats.event_queue_used_max = event_queue_count;
337 }
338 size_t event_queue_hwm = event_queue_get_high_water_mark();
339 if (event_queue_hwm > app->stats.event_queue_used_max)
340 {
341 app->stats.event_queue_used_max = event_queue_hwm;
342 }
346}
347
349{
350 bool tx_active = !tx_queue_is_empty(&app->tx_queue);
351 int max_events_per_iteration =
353
354 event_t event_item;
355 for (int i = 0; i < max_events_per_iteration && event_queue_pop(&event_item); i++)
356 {
357 event_dispatch(&event_item, app);
358 }
359}
360
366{
367 size_t tx_q_used = tx_queue_get_count(&app->tx_queue);
368
369 if (!runtime->udp_rx_throttled && tx_q_used >= TX_QUEUE_HIGH_WM)
370 {
371 if (runtime->udp_rx_throttled == false)
372 {
374 }
375 runtime->udp_rx_throttled = true;
376 }
377 else if (runtime->udp_rx_throttled && tx_q_used <= TX_QUEUE_LOW_WM)
378 {
379 runtime->udp_rx_throttled = false;
380 }
381}
382
384{
385 static event_loop_runtime_t event_loop_runtime = {.rx_byte = 0U,
386 .hdlc_decode_fail_streak = 0U,
387 .last_rx_byte_us = 0U,
388 .last_frame_ready_bytes = 0U,
389 .hunt_idle_run_length = 0U,
390 .work_done = false,
391 .udp_rx_throttled = false};
392
393 while (true)
394 {
395 watchdog_update();
396
397 // Poll the event queue
398 cli_poll();
399
400 // Drain RX early in the loop to reduce risk of RX FIFO stalling under continuous clock.
401 size_t rx_drained_early = 0;
402 drain_rx_until_empty(app, &event_loop_runtime, &rx_drained_early);
403 update_rx_drain_stats(app, &event_loop_runtime, rx_drained_early,
404 to_us_since_boot(get_absolute_time()));
405
408
409 (void)poll_tx_pipeline(app, &event_loop_runtime);
410 update_udp_rx_throttle_state(app, &event_loop_runtime);
411 if (!event_loop_runtime.udp_rx_throttled)
412 {
413 poll_and_enqueue_udp_rx(app, &event_loop_runtime);
414 }
415 else
416 {
418 }
419
420 // Drain RX FIFO into the accumulator buffer
421 size_t rx_drained = 0;
422 drain_rx_until_empty(app, &event_loop_runtime, &rx_drained);
423 uint64_t now_us = to_us_since_boot(get_absolute_time());
424 update_rx_drain_stats(app, &event_loop_runtime, rx_drained, now_us);
425
431 poll_hdlc_idle_timeout(app, &event_loop_runtime, now_us);
432
433 // Try to extract all complete HDLC frames currently buffered.
434 drain_hdlc_frames_to_udp(app, &event_loop_runtime);
435
436 poll_hdlc_no_progress(app, &event_loop_runtime);
437
440 {
442 }
443
446 {
447 app->need_prompt = true;
448 }
449 print_prompt(app);
450
451 if (!event_loop_runtime.work_done)
452 {
453 sleep_us(MAIN_LOOP_SLEEP_US);
454 }
455 else
456 {
457 event_loop_runtime.work_done = false;
458 }
459 }
460}
void cli_poll(void)
Poll USB CDC for CLI input, echo, and emit line events.
Definition cli_usb_cdc.c:40
#define LOG_DEBUG(...)
Definition common.h:165
bool log_take_emitted_flag(void)
Atomically read and clear "log emitted" flag.
Definition log.c:144
#define LOG_PLAIN(...)
Definition common.h:162
uint32_t log_take_dropped_count(void)
Definition log.c:132
#define PRIu64
Definition common.h:73
#define LOG_ERROR(...)
Definition common.h:163
uint32_t log_get_high_water_mark(void)
Definition log.c:139
e2s_error_t
Common error codes returned by ether2ser modules.
Definition error.h:27
@ E2S_ERR_HDLC_ACC_FRAME_READY
Definition error.h:67
@ E2S_OK
Definition error.h:28
@ E2S_ERR_TX_QUEUE_FULL
Definition error.h:76
void event_dispatch(const event_t *event, app_ctx_t *app)
Dispatch one event to the corresponding handler.
#define HDLC_SYNC_NO_PROGRESS_MAX_BYTES_EXTERNAL
Definition event_loop.c:54
static bool event_loop_should_drop_hunt_idle_byte(app_ctx_t *app, uint8_t rx_byte, uint32_t *idle_run_length)
Definition event_loop.c:65
#define MAIN_LOOP_SLEEP_US
Definition event_loop.c:43
static void update_rx_drain_stats(app_ctx_t *app, event_loop_runtime_t *event_loop_runtime, const size_t rx_drained_early, const uint64_t now_us)
Definition event_loop.c:125
static void print_prompt(app_ctx_t *app)
Definition event_loop.c:56
#define HDLC_SYNC_IDLE_TIMEOUT_US
Definition event_loop.c:52
static void drain_rx_until_empty(app_ctx_t *app, event_loop_runtime_t *runtime, size_t *rx_drained)
Definition event_loop.c:106
static void poll_hdlc_no_progress(app_ctx_t *app, event_loop_runtime_t *event_loop_runtime)
Definition event_loop.c:275
static void poll_and_dispatch_events(app_ctx_t *app)
Definition event_loop.c:348
#define HDLC_SYNC_NO_PROGRESS_MAX_BYTES
Definition event_loop.c:53
static void poll_hdlc_idle_timeout(app_ctx_t *app, event_loop_runtime_t *event_loop_runtime, const uint64_t now_us)
Definition event_loop.c:194
static void poll_and_enqueue_udp_rx(app_ctx_t *app, event_loop_runtime_t *event_loop_runtime)
Definition event_loop.c:140
static void update_udp_rx_throttle_state(app_ctx_t *app, event_loop_runtime_t *runtime)
Definition event_loop.c:365
static void drain_hdlc_frames_to_udp(app_ctx_t *app, event_loop_runtime_t *event_loop_runtime)
Definition event_loop.c:257
#define TX_QUEUE_DRAIN_CHUNK_SIZE
Definition event_loop.c:45
#define EVENT_LOOP_MAX_EVENTS_AT_ONCE
Definition event_loop.c:49
static size_t poll_tx_pipeline(app_ctx_t *app, event_loop_runtime_t *event_loop_runtime)
Definition event_loop.c:166
#define EVENT_LOOP_MAX_EVENTS_WHILE_TX_ACTIVE
Definition event_loop.c:50
#define TX_QUEUE_HIGH_WM
Definition event_loop.c:46
#define HDLC_DECODE_FAIL_STREAK_LIMIT
Definition event_loop.c:51
void event_loop(app_ctx_t *app)
Run the main application polling loop.
Definition event_loop.c:383
static void update_statistics(app_ctx_t *app)
Definition event_loop.c:312
#define TX_QUEUE_LOW_WM
Definition event_loop.c:47
static void decode_hdlc_to_udp_tx(app_ctx_t *app, event_loop_runtime_t *event_loop_runtime)
Definition event_loop.c:212
uint32_t event_queue_get_push_drop_count(void)
Get cumulative number of dropped push attempts.
Definition event_queue.c:99
bool event_queue_pop(event_t *event_out)
Dequeue an event.
Definition event_queue.c:67
size_t event_queue_get_count(void)
Get current number of queued events.
Definition event_queue.c:89
size_t event_queue_get_high_water_mark(void)
Get peak queue fill count observed since init.
Definition event_queue.c:94
#define HDLC_FLAG_BYTE
Definition hdlc_common.h:29
bool hdlc_decode(const HDLC_FRAME_T *frame, uint8_t *payload, const size_t out_capacity, size_t *payload_length, bool lsb_first)
Decode an HDLC bit-stuffed frame.
void hdlc_decode_stats_snapshot(hdlc_decode_stats_t *out_stats)
Copy cumulative HDLC decode statistics.
void hdlc_sync_acc_consume_candidate(HDLC_SYNC_ACCUMULATOR_T *accumulator, bool accept)
Consume current candidate and advance accumulator window.
Definition hdlc_sync.c:622
void hdlc_sync_acc_init(HDLC_SYNC_ACCUMULATOR_T *accumulator, uint8_t sync_byte)
Initialize HDLC sync accumulator state.
Definition hdlc_sync.c:507
e2s_error_t hdlc_sync_acc_poll(HDLC_SYNC_ACCUMULATOR_T *accumulator, HDLC_FRAME_T *out_frame)
Poll accumulator for an aligned HDLC frame candidate.
Definition hdlc_sync.c:549
bool hdlc_sync_acc_process_byte(HDLC_SYNC_ACCUMULATOR_T *accumulator, uint8_t byte)
Append one received raw byte to the accumulator.
Definition hdlc_sync.c:527
@ HDLC_SYNC_STATE_HUNTING
Definition hdlc_sync.h:43
bool tx_poll(void)
Poll TX completion/holdoff state and manage RTS release.
bool rx_get(uint8_t *data)
Read one received byte from RX PIO FIFO.
bool rx_clock_poll_stall(void)
Poll RX FIFO stall events and update the provided event counter.
void rx_clock_hard_reset(void)
Disable RX Path, clear fifos, restart SMs and CLKDIV and enable again.
size_t length
Definition hdlc_common.h:42
uint8_t * payload
Definition hdlc_common.h:40
uint32_t lookahead_wait_synced
Definition hdlc_sync.h:75
uint32_t hardcap_drop_bytes
Definition hdlc_sync.h:79
uint32_t lookahead_wait_syncing
Definition hdlc_sync.h:74
HDLC_SYNC_STATE_T state
Definition hdlc_sync.h:71
uint32_t hardcap_drop_events
Definition hdlc_sync.h:78
size_t length
uint8_t * payload
bool external_clock
Definition v24_config.h:97
Global application context shared across modules.
Definition app_context.h:98
UDP_FRAME_T tx_frame_buffer
bool need_prompt
UDP_FRAME_T rx_frame_buffer
HDLC_SYNC_ACCUMULATOR_T accumulator
TX_QUEUE_T tx_queue
payload_statistics_t stats
UDP_CONFIG_T sender_config
V24_CONFIG_T v24_config
UDP_CONFIG_T destination_config
HDLC_FRAME_T reconstructed_frame
uint8_t hdlc_decode_fail_streak
Definition event_loop.c:98
uint64_t last_rx_byte_us
Definition event_loop.c:99
uint64_t last_frame_ready_bytes
Definition event_loop.c:100
uint32_t hunt_idle_run_length
Definition event_loop.c:101
Event queue entry.
Definition event_queue.h:91
Cumulative HDLC decode failure reason counters.
uint64_t payload_too_long
uint64_t rx_fifo_stall_events
Definition app_context.h:87
uint64_t sync_candidate_consume
Definition app_context.h:65
uint64_t hdlc_decode_fail
Definition app_context.h:56
uint64_t tx_queue_used_max
Definition app_context.h:79
uint64_t resync_no_progress_count
Definition app_context.h:77
uint64_t decode_fail_crc_mismatch
Definition app_context.h:73
uint64_t sync_hardcap_drop_events
Definition app_context.h:66
uint64_t udp_rx_throttle_enter
Definition app_context.h:58
uint64_t tx_queue_drop_frames
Definition app_context.h:80
uint64_t accumulator_pos_max
Definition app_context.h:86
uint64_t hunt_idle_drop_bytes
Definition app_context.h:89
uint64_t event_queue_drop_events
Definition app_context.h:82
uint64_t decode_fail_unstuff_error
Definition app_context.h:72
uint64_t udp_rx_throttle_skips
Definition app_context.h:59
uint64_t log_queue_used_max
Definition app_context.h:84
uint64_t hdlc_frame_ready
Definition app_context.h:54
uint64_t serial_rx_drop_acc_full
Definition app_context.h:88
uint64_t decode_fail_invalid_frame
Definition app_context.h:69
uint64_t event_queue_used_max
Definition app_context.h:81
uint64_t decode_fail_too_short
Definition app_context.h:70
uint64_t sync_hardcap_drop_bytes
Definition app_context.h:67
uint64_t resync_idle_timeout_count
Definition app_context.h:75
uint64_t sync_lookahead_wait_syncing
Definition app_context.h:63
uint64_t resync_hard_fail_count
Definition app_context.h:76
uint64_t udp_tx_buffer_full_counts
Definition app_context.h:61
uint64_t decode_fail_payload_too_long
Definition app_context.h:71
uint64_t sync_lookahead_wait_synced
Definition app_context.h:64
uint64_t udp_rx_buffer_full_counts
Definition app_context.h:60
e2s_error_t poll_queue_stats(TX_QUEUE_T *queue)
Emit queue usage statistics when needed.
Definition tx_queue.c:49
bool tx_queue_is_empty(TX_QUEUE_T *queue)
Check whether queue and active entry are fully drained.
Definition tx_queue.c:122
size_t tx_queue_get_count(const TX_QUEUE_T *queue)
Get current number of queued entries.
Definition tx_queue.c:113
e2s_error_t tx_queue_enqueue_udp_frame(TX_QUEUE_T *queue, const UDP_FRAME_T *frame)
Encode UDP frame to HDLC and append it to TX queue.
Definition tx_queue.c:201
e2s_error_t tx_queue_drain(TX_QUEUE_T *queue, size_t bytes_to_drain, size_t *bytes_drained)
Drain up to bytes_to_drain bytes from queue into TX FIFO.
Definition tx_queue.c:129
void w5500_poll_udp_buffer_full_events(uint64_t *rx_full_enter_events, uint64_t *tx_full_enter_events)
void w5500_udp_tx(UDP_CONFIG_T *send_config, const UDP_FRAME_T *frame)
Send one UDP frame through W5500.
bool w5500_poll_rx(UDP_CONFIG_T *send_config, UDP_FRAME_T *frame)
Poll W5500 for received UDP data.
#define TX_BUF_SIZE