继 Amazon 将 FreeRTOS 收入麾下之后,微软于 2019 年 4 月将 Express Logic 收购, ThreadX 随之更名为 Azure RTOS ThreadX。 本文以 Azure RTOS ThreadX 为例,说明如何在一种新的 RTOS 上运行 NoOS 项目。
首先创建一个新项目(开发环境可随意选择,ThreadX 全部兼容。),RTOS Options 选择“No RTOS”。下载 ThreadX 最新版, 将 common、ports\cortex_m3、utility\low_power 等三部分代码添加到我们的项目里。
初识 ThreadX
ThreadX 具有几个鲜明的特点:
- (几乎)一个函数对应一个独立的源文件,醒目、直接;
- 模块化程度高,比如,内存管理
byte_allocate
模块可以去除; - 每个 API 都存在两个版本,一个以
_txe_
开头,包含详尽的参数检查,适合调试、学习;另一个是以_tx
开头的“正常”版本; 调用 API 时只用tx_
开头的宏版本,这个宏版本会依照编译选项映射成_txe_
或者_tx
; - 其它特性(如 preemption-threshold, event chaining, 性能分析等)跟移植关系不大,不再赘述。
移植 _tx_initialize_low_level
从 ThreadX 示例项目的启动文件里找到这个函数,只保留栈指针相关内容:
EXPORT _tx_initialize_low_level
_tx_initialize_low_level
IMPORT _tx_thread_system_stack_ptr
CPSID i
; Set system stack pointer from vector value.
LDR r0, =_tx_thread_system_stack_ptr
LDR r1, =__Vectors
LDR r1, [r1]
STR r1, [r0]
BX lr
SysTick 和 MCU 异常优先级配置重新封装成两个函数:
void _setup_sys_handlers(void)
{
// Setup System Handlers 4-7 Priority Registers
io_write(NVIC_ADDR + 0xD18, 0x00000000);
// SVCl, Rsrv, Rsrv, Rsrv
// Setup System Handlers 8-11 Priority Registers
// Note: SVC must be lowest priority, which is 0xFF
io_write(NVIC_ADDR + 0xD1C, 0xFF000000);
// SysT, PnSV, Rsrv, DbgM
// Setup System Handlers 12-15 Priority Registers
// Note: PnSV must be lowest priority, which is 0xFF
io_write(NVIC_ADDR + 0xD20, 0x40FF0000);
}
#define TICK_PER_SECOND 1024
#define RTC_CYCLES_PER_TICK (RTC_CLK_FREQ / TICK_PER_SECOND)
void _systick_init(void)
{
portNVIC_SYSTICK_LOAD_REG = RTC_CYCLES_PER_TICK - 1;
portNVIC_SYSTICK_CURRENT_VALUE_REG = 0;
portNVIC_SYSTICK_CTRL_REG = portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT | portNVIC_SYSTICK_CLK_BIT;
}
下面是通用 OS 接口在 ThreadX 上的具体实现。
堆相关 API
我们分配一块大小确定的内存作为堆。
#ifndef RTOS_HEAP_SIZE
#define RTOS_HEAP_SIZE (20 * 1024)
#endif
static uint32_t heap[RTOS_HEAP_SIZE / sizeof(uint32_t)];
static TX_BYTE_POOL pool;
tx_byte_pool_create(&pool, NULL, heap, sizeof(heap));
实现一对分配、释放函数:
static void *port_malloc(size_t size)
{
void *p;
return tx_byte_allocate(&pool, &p, size, 0) == TX_SUCCESS ? p : NULL;
}
static void port_free(void *p)
{
tx_byte_release(p);
}
查看 ThreadX 源代码可知,这对函数是线程安全的。
软件定时器相关 API
软件定时器的存储空间由我们在堆上分配,然后将指针作为 gen_handle_t
。具体实现如下:
gen_handle_t port_timer_create(
uint32_t timeout_in_ms,
void *user_data,
void (* timer_cb)(void *)
)
{
TX_TIMER *timer = (TX_TIMER *)port_malloc(sizeof(TX_TIMER));
tx_timer_create(timer, NULL,
(fun_void_ul_f)timer_cb, (ULONG)user_data,
ms_to_ticks(timeout_in_ms), 0, TX_NO_ACTIVATE);
return timer;
}
void port_timer_start(gen_handle_t timer)
{
TX_TIMER *t = (TX_TIMER *)timer;
tx_timer_activate(t);
}
void port_timer_stop(gen_handle_t timer)
{
TX_TIMER *t = (TX_TIMER *)timer;
tx_timer_deactivate(t);
}
void port_timer_delete(gen_handle_t timer)
{
TX_TIMER *t = (TX_TIMER *)timer;
tx_timer_delete(t);
port_free(t);
}
任务相关 API
ThreadX 里,优先级数目越大,优先级越低,最高为 0,最低为 (TX_MAX_PRIORITIES - 1)。参考实现:
// ThreadX: Numerically smaller values imply higher priority
#define APP_PRIO_LOW 4
#define APP_PRIO_HIGH 2
gen_handle_t port_task_create(
const char *name,
void (*entry)(void *),
void *parameter,
uint32_t stack_size, // stack size in bytes
enum gen_os_task_priority priority
)
{
TX_THREAD *thread = (TX_THREAD *)port_malloc(sizeof(TX_THREAD));
VOID *stack = port_malloc(stack_size);
tx_thread_create(thread, (char *)name,
(fun_void_ul_f)entry, (ULONG)parameter,
stack, stack_size,
priority == GEN_TASK_PRIORITY_LOW ? APP_PRIO_LOW : APP_PRIO_HIGH,
2,
TX_NO_TIME_SLICE, TX_AUTO_START);
return (gen_handle_t)thread;
}
消息队列相关 API
ThreadX 里消息长度必须是 4 字节的倍数,最长为 64 字节(TX_16_ULONG)。 通用 OS 接口里消息大小可能不是 4 字节的倍数,不过好在不会超过 64 字节(虽然文档里没注明)。
基于这些考虑,在从 ThreadX 取消息时,为了防止内存溢出,事先准备一段足够长度内存空间。实现如下:
typedef struct
{
TX_QUEUE queue;
int msg_len;
uint32_t msg[];
} tx_queue_sup_t;
gen_handle_t port_queue_create(int len, int item_size)
{
// msg size is in 32-bit words
int word_size = (item_size + 3) >> 2;
int queue_byte_size = len * word_size * 4;
tx_queue_sup_t *queue = (tx_queue_sup_t *)port_malloc(sizeof(tx_queue_sup_t) + word_size * 4);
queue->msg_len = item_size;
VOID *queue_start = (VOID *)port_malloc(queue_byte_size);
if (tx_queue_create(&queue->queue, NULL, word_size, queue_start, queue_byte_size) != TX_SUCCESS)
platform_raise_assertion(__FILE__, __LINE__);
return (gen_handle_t)queue;
}
int port_queue_send_msg(gen_handle_t queue, void *msg)
{
tx_queue_sup_t *q = (tx_queue_sup_t *)queue;
return tx_queue_send(&q->queue, msg, TX_WAIT_FOREVER);
}
// return 0 if msg received; otherwise failed (timeout)
int port_queue_recv_msg(gen_handle_t queue, void *msg)
{
tx_queue_sup_t *q = (tx_queue_sup_t *)queue;
if (TX_SUCCESS == tx_queue_receive(&q->queue, q->msg, TX_WAIT_FOREVER))
{
memcpy(msg, q->msg, q->msg_len);
return 0;
}
else
return 1;
}
事件相关 API
使用 ThreadX 的 EVENT_FLAGS 实现事件接口,设置事件时对应于 ThreadX 的 OR 操作:
gen_handle_t port_event_create()
{
TX_EVENT_FLAGS_GROUP *group = (TX_EVENT_FLAGS_GROUP *)port_malloc(sizeof(TX_EVENT_FLAGS_GROUP));
tx_event_flags_create(group, NULL);
return (gen_handle_t)group;
}
// return 0 if msg received; otherwise failed (timeout)
int port_event_wait(gen_handle_t event)
{
ULONG actual_flags_ptr;
return tx_event_flags_get((TX_EVENT_FLAGS_GROUP *)event, 1,
TX_AND_CLEAR, &actual_flags_ptr, TX_WAIT_FOREVER);
}
// event_set(event) will release the task in waiting.
void port_event_set(gen_handle_t event)
{
tx_event_flags_set((TX_EVENT_FLAGS_GROUP *)event, 1, TX_OR);
}
全局临界区 API
可以借助 TX_DISABLE
、TX_RESTORE
配合计数器实现。
OS 入口 API
ThreadX 的入口是 tx_kernel_enter
。把它拆成两个部分:一部分在 app_main
返回 platform
之前执行,此时 ThreadX 允许创建线程;后一部分封装成通用 OS 的入口。
// 前一部分
static void port_tx_initialize_kernel(void)
{
// ...
_systick_init();
_setup_sys_handlers();
_tx_thread_system_state = TX_INITIALIZE_IN_PROGRESS;
}
// 后一部分
static void port_tx_start(void)
{
_tx_thread_system_state = TX_INITIALIZE_IS_FINISHED;
/// ...
#ifdef TX_SAFETY_CRITICAL
/* If we ever get here, raise safety critical exception. */
TX_SAFETY_CRITICAL_EXCEPTION(__FILE__, __LINE__, 0);
#endif
}
整合
const gen_os_driver_t gen_os_driver =
{
.timer_create = port_timer_create,
.timer_start = port_timer_start,
.timer_stop = port_timer_stop,
.timer_delete = port_timer_delete,
.task_create = port_task_create,
.queue_create = port_queue_create,
.queue_send_msg = port_queue_send_msg,
.queue_recv_msg = port_queue_recv_msg,
.event_create = port_event_create,
.event_set = port_event_set,
.event_wait = port_event_wait,
.malloc = port_malloc,
.free = port_free,
.enter_critical = port_enter_critical,
.leave_critical = port_leave_critical,
.os_start = port_tx_start,
.tick_isr = _tx_timer_interrupt,
.svc_isr = no_op,
.pendsv_isr = __tx_PendSVHandler,
};
const gen_os_driver_t *os_impl_get_driver(void)
{
tx_byte_pool_create(&pool, NULL, heap, sizeof(heap));
port_tx_initialize_kernel();
return &gen_os_driver;
}
ThreadX 目前不使用 SVC 中断,no_op
是一个桩函数。
测试
参照 SDK 里 Peripheral Console
项目的 profile.c
、service_console.c
加入项目,然后参照
Peripheral Console (RT-Thread)
修改 main.c
,准备一条欢迎信息:
#define expand2(X) #X
#define expand(X) expand2(X)
const char welcome_msg[] = "Built with Azure ThreadX (" expand(THREADX_MAJOR_VERSION) "."
expand(THREADX_MINOR_VERSION) "." expand(THREADX_PATCH_VERSION) ")";
编译、下载、测试。
低功耗
ThreadX 默认的低功耗实现方式不合适,需要自行创建一个空闲线程,实现“无滴答”低功耗。空闲线程的具体实现与
Peripheral Console (RT-Thread)
类似,主要流程如下:
void thread_idle_entry(ULONG thread_input)
{
for (;;)
{
tx_timer_get_next(&tx_low_power_next_expiration);
tx_low_power_next_expiration = platform_pre_suppress_ticks_and_sleep_processing(tx_low_power_next_expiration);
tx_low_power_adjust_ticks = rtos_sleep_process(tx_low_power_next_expiration);
tx_time_increment(tx_low_power_adjust_ticks);
platform_os_idle_resumed_hook();
}
}
至此,蓝牙协议栈、低功耗等两项功能都在 ThreadX 上运行起来了。
下载
直接 下载 完整的 Keil 5 项目代码。