/* * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ #include #include #include #include #include #include #include #include "string.h" #include "FreeRTOS.h" #include "task.h" #include "utils/wait_for_event.h" #include "esp_private/freertos_linux_coop_syscalls.h" #include "utils/linux_port_utils.h" #define FREERTOS_SIM_TICK_PERIOD_US (1000000 / CONFIG_FREERTOS_HZ) typedef struct thread { const char *name; pthread_t pthread; TaskFunction_t pxCode; void *pvParams; bool is_dying; bool yield_needed; struct event *ev; } thread_t; typedef struct task_thread_node { TaskHandle_t handle; thread_t *thread; SLIST_ENTRY(task_thread_node) next; } task_thread_node_t; static SLIST_HEAD(task_thread_node_ll, task_thread_node) s_task_thread_list = SLIST_HEAD_INITIALIZER(task_thread_node); static pthread_mutex_t s_thread_map_mutex = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t s_port_mutex; static pthread_t s_scheduler_thread; static bool s_scheduler_started = false; static int s_ux_critical_nesting = 0; /* TLS flag: true only when inside a real FreeRTOS task pthread */ static __thread bool s_in_freertos_task = false; bool linux_port_in_freertos_task(void) { return s_in_freertos_task; } static void linux_port_initialize_mutexes(void) { pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init(&s_port_mutex, &attr); pthread_mutexattr_destroy(&attr); } static void linux_port_fatal_error(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); abort(); } static void linux_port_register_thread(TaskHandle_t handle, thread_t *thread) { if (handle == NULL) { return; } task_thread_node_t *node = malloc(sizeof(task_thread_node_t)); if (!node) { linux_port_fatal_error("Failed to allocate thread map node", -1); } node->handle = handle; node->thread = thread; pthread_mutex_lock(&s_thread_map_mutex); SLIST_INSERT_HEAD(&s_task_thread_list, node, next); pthread_mutex_unlock(&s_thread_map_mutex); } static void linux_port_unregister_thread(TaskHandle_t handle) { if (handle == NULL) { return; } pthread_mutex_lock(&s_thread_map_mutex); task_thread_node_t *cur_node = SLIST_FIRST(&s_task_thread_list); task_thread_node_t *prev_node = NULL; while (cur_node) { if (cur_node->handle == handle) { if (prev_node) { prev_node->next.sle_next = SLIST_NEXT(cur_node, next); } else { SLIST_REMOVE_HEAD(&s_task_thread_list, next); } free(cur_node); pthread_mutex_unlock(&s_thread_map_mutex); return; } prev_node = cur_node; cur_node = SLIST_NEXT(cur_node, next); } pthread_mutex_unlock(&s_thread_map_mutex); } static thread_t *linux_port_get_thread_from_handle(TaskHandle_t handle) { if (handle == NULL) { return NULL; } pthread_mutex_lock(&s_thread_map_mutex); task_thread_node_t *node = NULL; SLIST_FOREACH(node, &s_task_thread_list, next) { if (node->handle == (TaskHandle_t)(*(StackType_t **)(handle))) { thread_t *t = node->thread; pthread_mutex_unlock(&s_thread_map_mutex); return t; } } pthread_mutex_unlock(&s_thread_map_mutex); return NULL; } static thread_t *linux_port_get_calling_thread(void) { pthread_t self = pthread_self(); pthread_mutex_lock(&s_thread_map_mutex); task_thread_node_t *node = NULL; SLIST_FOREACH(node, &s_task_thread_list, next) { if (pthread_equal(node->thread->pthread, self)) { thread_t *thread = node->thread; pthread_mutex_unlock(&s_thread_map_mutex); return thread; } } pthread_mutex_unlock(&s_thread_map_mutex); return NULL; } pthread_t linux_port_get_scheduled_task_pthread(void) { thread_t *thread = linux_port_get_thread_from_handle(xTaskGetCurrentTaskHandle()); return thread ? thread->pthread : pthread_self(); } static void *linux_port_task_runner(void *arg) { /* Allow this thread to be cancelled */ pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); /* set the flag showing that this is a freertos task */ s_in_freertos_task = true; /* setup the backtrace signal. ONLY triggered before abort so * it will not interfere with the simulation while its running */ linux_port_setup_backtrace_signal(); thread_t *thread = arg; /* Block until scheduler signals first time, then run the task body. */ event_wait(thread->ev); thread->pxCode(thread->pvParams); return NULL; } static void linux_port_unblock_thread(thread_t *thread) { event_signal(thread->ev); } static void linux_port_block_thread(thread_t *thread) { event_wait(thread->ev); } static void linux_port_increment_tick(void) { (void)xTaskIncrementTick(); } static void linux_port_switch_context(TaskHandle_t current_task_hdl) { pthread_mutex_lock(&s_port_mutex); thread_t *current_thread = linux_port_get_thread_from_handle(current_task_hdl); /* get the task that should be scheduled next */ vTaskSwitchContext(); /* get the new task to schedule and the associated thread item */ TaskHandle_t next_task_hdl = xTaskGetCurrentTaskHandle(); thread_t *next_thread = linux_port_get_thread_from_handle(next_task_hdl); /* unblock the newly scheduled task if it is different from * the one already scheduled */ if (next_thread) { /* Only unblock the thread if we are actually switching to a * different one. Signaling the already-running thread would * leave a stale event_triggered flag, causing its next * event_wait (e.g. in vPortYield) to return immediately * instead of blocking. * * Exception: on the very first switch, the task is still blocked * in its initial event_wait (linux_port_task_runner), so we must * signal it even though current_thread == next_thread. */ if (next_thread != current_thread || !s_scheduler_started) { /* Discard any stale wake on the task being switched out. * * Because tasks are pthreads that cannot be forcibly paused, * the port can only "unschedule" a task by choosing a different * next task; the outgoing task keeps running until it next * blocks itself in event_wait (e.g. via vPortYield). If, while * it was unscheduled, the scheduler already switched back into * it (event_signal in a later switch) without the task ever * having parked in event_wait, that signal is never consumed * and event_triggered stays latched. Its next voluntary block * would then return immediately instead of blocking (e.g. * vTaskDelay(100ms) returning in 0ms). * * Clearing here, under s_port_mutex and atomically with the * scheduling decision, drops that latched-but-unconsumed wake. * It is safe: a legitimate wake can only be delivered later, * when the scheduler next selects this task as next_thread. * * Reached only for a genuine switch to a different task: * entering this block with next_thread == current_thread * happens solely on the first switch (!s_scheduler_started), * which the current_thread NULL-check below also tolerates. * The NULL-check additionally guards the deleted-task path * where the outgoing task has no thread mapping. */ if (current_thread) { event_clear(current_thread->ev); } linux_port_unblock_thread(next_thread); } } if (!s_scheduler_started) { s_scheduler_started = true; } /* fill the name of the task in the thread item if not done already. */ if (next_thread && next_thread->name == NULL) { /* fill the name of the thread now */ next_thread->name = pcTaskGetName(next_task_hdl); } /* fill the name of the task in the thread item if not done already. */ if (current_thread && current_thread->name == NULL) { /* fill the name of the thread now */ current_thread->name = pcTaskGetName(current_task_hdl); } pthread_mutex_unlock(&s_port_mutex); } static void *linux_port_scheduler_runner(void *arg) { (void)arg; while (1) { /* sleep for a period of 1 tick */ usleep(FREERTOS_SIM_TICK_PERIOD_US); /* get the task that is currently scheduled */ TaskHandle_t current_task_hdl = xTaskGetCurrentTaskHandle(); /* Lock the port mutex. This will block while any task is in a * critical section, ensuring ticks don't preempt critical code. */ pthread_mutex_lock(&s_port_mutex); /* increment the freertos tick */ linux_port_increment_tick(); /* schedule a new task, and schedule out the currently running one */ linux_port_switch_context(current_task_hdl); pthread_mutex_unlock(&s_port_mutex); } return NULL; } StackType_t *pxPortInitialiseStack(StackType_t *pxTopOfStack, StackType_t *pxEndOfStack, TaskFunction_t pxCode, void *pvParameters) { pthread_attr_t thread_attr; size_t thread_stack_size; /* Store the thread data at the start of the stack. */ thread_stack_size = (pxTopOfStack - pxEndOfStack) * sizeof(*pxTopOfStack); pthread_attr_init(&thread_attr); pthread_attr_setstack(&thread_attr, pxEndOfStack, thread_stack_size); thread_t *thread = malloc(sizeof(thread_t)); if (!thread) { linux_port_fatal_error("Failed to allocate thread metadata", -1); } thread->name = NULL; // this will be filled later when we know about the task name thread->pxCode = pxCode; thread->pvParams = pvParameters; thread->is_dying = false; thread->yield_needed = false; thread->ev = event_create(); linux_port_register_thread((TaskHandle_t)pxTopOfStack, thread); /* create the thread associated with the task being created */ const int ret = pthread_create(&thread->pthread, &thread_attr, linux_port_task_runner, thread); if (ret != 0) { linux_port_fatal_error("pthread_create", ret); } return pxTopOfStack; } BaseType_t xPortStartScheduler(void) { /* set the port mutex to be recursive. Must be done before * vPortEnableInterrupts() which calls vPortExitCritical(). */ linux_port_initialize_mutexes(); /* enable interrupt that were disabled in vTaskStartScheduler */ vPortEnableInterrupts(); /* init the cooperative syscall layer (sets stdio non-blocking). * Provided by VFS component; weak no-op when VFS is not linked. */ freertos_linux_coop_syscalls_init(); /* Start scheduler thread */ int ret = pthread_create(&s_scheduler_thread, NULL, linux_port_scheduler_runner, NULL); if (ret != 0) { linux_port_fatal_error("pthread_create", ret); } /* Should never return */ pthread_join(s_scheduler_thread, NULL); return 0; } void vPortEndScheduler(void) { exit(0); } void vPortEnterCritical(void) { if (!s_scheduler_started) { return; } pthread_mutex_lock(&s_port_mutex); /* Non-FreeRTOS thread or recursive enter: just bump the counter. * The mutex is already held (recursive lock succeeds for same thread). */ if (!linux_port_in_freertos_task() || s_ux_critical_nesting > 0) { s_ux_critical_nesting++; return; } /* First enter from a FreeRTOS task: ensure we're the scheduled task. * If not, release the mutex, block until the scheduler switches to us, * then re-acquire. */ thread_t *calling_thread = linux_port_get_calling_thread(); thread_t *scheduled_thread = linux_port_get_thread_from_handle(xTaskGetCurrentTaskHandle()); while (calling_thread && !calling_thread->is_dying && calling_thread != scheduled_thread) { pthread_mutex_unlock(&s_port_mutex); linux_port_block_thread(calling_thread); pthread_mutex_lock(&s_port_mutex); calling_thread = linux_port_get_calling_thread(); scheduled_thread = linux_port_get_thread_from_handle(xTaskGetCurrentTaskHandle()); } if (!calling_thread || calling_thread->is_dying) { linux_port_switch_context(xTaskGetCurrentTaskHandle()); pthread_mutex_unlock(&s_port_mutex); return; } s_ux_critical_nesting = 1; } void vPortExitCritical(void) { if (!s_scheduler_started || s_ux_critical_nesting == 0) { return; } s_ux_critical_nesting--; /* Check for deferred yield on final exit from a FreeRTOS task */ if (s_ux_critical_nesting == 0 && linux_port_in_freertos_task()) { thread_t *calling_thread = linux_port_get_calling_thread(); if (calling_thread && calling_thread->yield_needed) { calling_thread->yield_needed = false; pthread_mutex_unlock(&s_port_mutex); vPortYield(); return; } } pthread_mutex_unlock(&s_port_mutex); } /* Handle the case where the calling pthread is not a registered FreeRTOS task. * If the calling thread has been deleted but is still the scheduled task, * perform a context switch. Otherwise just release the mutex and return. * Returns true if the caller should return early. */ static bool linux_port_handle_deleted_task(TaskHandle_t scheduled_task_hdl, thread_t *calling_thread) { if (calling_thread != NULL) { return false; } thread_t *scheduled_thread = linux_port_get_thread_from_handle(scheduled_task_hdl); if (scheduled_thread == NULL) { linux_port_switch_context(scheduled_task_hdl); } pthread_mutex_unlock(&s_port_mutex); return true; } void vPortYield(void) { pthread_mutex_lock(&s_port_mutex); thread_t *calling_thread = linux_port_get_calling_thread(); TaskHandle_t scheduled_task_hdl = xTaskGetCurrentTaskHandle(); if (linux_port_handle_deleted_task(scheduled_task_hdl, calling_thread)) { return; } /* If in a critical section, defer the yield until the section exits. */ if (s_ux_critical_nesting != 0) { calling_thread->yield_needed = true; pthread_mutex_unlock(&s_port_mutex); return; } /* Hand the CPU to the next ready task right now (mimics PendSV on real * hardware). */ linux_port_switch_context(scheduled_task_hdl); pthread_mutex_unlock(&s_port_mutex); /* If the newly scheduled task is different from the calling thread, * block until the scheduler resumes this task. */ TaskHandle_t next_task_hdl = xTaskGetCurrentTaskHandle(); thread_t *next_thread = linux_port_get_thread_from_handle(next_task_hdl); if (calling_thread != next_thread) { linux_port_block_thread(calling_thread); } } void vPortYieldWithinApi(void) { vPortYield(); } void vPortSuspendScheduler(void) { /* scheduled out task trying to suspend the scheduler should get blocked here */ pthread_mutex_lock(&s_port_mutex); /* get the metadata of the pthread calling this function */ thread_t *calling_thread = linux_port_get_calling_thread(); /* get the thread metadata from the scheduled task */ TaskHandle_t scheduled_task_hdl = xTaskGetCurrentTaskHandle(); if (linux_port_handle_deleted_task(scheduled_task_hdl, calling_thread)) { return; } thread_t *scheduled_thread = linux_port_get_thread_from_handle(scheduled_task_hdl); if (calling_thread != scheduled_thread) { pthread_mutex_unlock(&s_port_mutex); vPortYield(); return; } pthread_mutex_unlock(&s_port_mutex); } void vPortDisableInterrupts(void) { vPortEnterCritical(); } void vPortEnableInterrupts(void) { vPortExitCritical(); } BaseType_t xPortSetInterruptMask(void) { vPortEnterCritical(); return pdTRUE; } void vPortClearInterruptMask(BaseType_t xMask) { vPortExitCritical(); } void vPortThreadDying(void *pxTaskToDelete, volatile BaseType_t *pxPendYield) { pthread_mutex_lock(&s_port_mutex); thread_t *thread = linux_port_get_thread_from_handle((TaskHandle_t)pxTaskToDelete); if (thread == NULL) { pthread_mutex_unlock(&s_port_mutex); return; } /* Mark the thread as dying, cancel the thread. the pthread * will be stopped on next cancellation point. Do not remove the * thread item from the list since it will be done in vPortCancelThread */ thread->is_dying = true; pthread_cancel(thread->pthread); pthread_mutex_unlock(&s_port_mutex); } #if CONFIG_FREERTOS_TLSP_DELETION_CALLBACKS static void vPortTLSPointersDelCb(void *pxTCB) { StaticTask_t *tcb = (StaticTask_t *)pxTCB; TlsDeleteCallbackFunction_t *pvDelCbs = (TlsDeleteCallbackFunction_t *)(&tcb->pvDummy15[configNUM_THREAD_LOCAL_STORAGE_POINTERS / 2]); for (int x = 0; x < (configNUM_THREAD_LOCAL_STORAGE_POINTERS / 2); x++) { if (pvDelCbs[x] != NULL) { pvDelCbs[x](x, tcb->pvDummy15[x]); } } } #endif /* CONFIG_FREERTOS_TLSP_DELETION_CALLBACKS */ void vPortCancelThread(void *pxTaskToDelete) { pthread_mutex_lock(&s_port_mutex); #if CONFIG_FREERTOS_TLSP_DELETION_CALLBACKS vPortTLSPointersDelCb(pxTaskToDelete); #endif thread_t *thread = linux_port_get_thread_from_handle((TaskHandle_t)pxTaskToDelete); if (!thread) { pthread_mutex_unlock(&s_port_mutex); return; } if (thread->is_dying) { /* vPortThreadDying already called */ } else { thread->is_dying = true; pthread_cancel(thread->pthread); } /* Save fields and unregister while holding the lock. */ pthread_t pt = thread->pthread; event_t *ev = thread->ev; linux_port_unregister_thread((TaskHandle_t)pxTaskToDelete); /* Release the mutex before joining – the dying thread may need the * scheduler (which also takes s_port_mutex) to reach a cancellation * point. */ pthread_mutex_unlock(&s_port_mutex); pthread_join(pt, NULL); event_delete(ev); free(thread); } void vPortSetStackWatchpoint(void *pxStackStart) { } #if ( CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS ) configRUN_TIME_COUNTER_TYPE xPortGetRunTimeCounterValue( void ) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return ( configRUN_TIME_COUNTER_TYPE ) ( ( ( uint64_t ) ts.tv_sec * 1000000ULL ) + ( ( uint64_t ) ts.tv_nsec / 1000ULL ) ); } #endif /* CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS */