☰
STM32 ThreadX串口驱动:DMA空闲中断与环形缓冲分层
2026/10/26 18:50:05 网站建设 项目流程

简介:面向嵌入式开发者的ThreadX串口通信实践工程包,基于STM32CubeMX生成,围绕UART的DMA收发展开,并引入队列与信号量完成中断与任务间的数据传递和同步,适合已掌握STM32基础、希望进一步理解实时操作系统任务通信机制的学习者参考。压缩包共2000个文件,约15.36MB,其中C源文件905个、头文件345个,另有汇编启动文件、链接脚本、静态库、工程配置及编译输出等,较完整地保留了从外设初始化、驱动实现到构建产物的工程结构。已有275人学习,读者可据此梳理ThreadX下串口DMA的配置流程、队列与信号量的典型用法,并对照工程文件组织方式排查中断优先级、缓冲管理与任务同步中的常见问题,从而对串口DMA收发、队列投递与信号量同步等关键环节形成直观认识,快速搭建可复用的串口通信框架。

1. 中断里只留三行代码:UART_ThreadX 这份工程在解决什么

裸机项目里最常见的写法,是在 USART 中断回调里直接拼协议、判帧头、顺手打印一句日志。波特率一上到 115200、帧长一过 32 字节,丢包就变成一种难以解释的现象:今天测试正常,明天现场就掉线。这份 UART_ThreadX 压缩包走的是另一条路:STM32CubeMX 负责把时钟、GPIO、USART、DMA 初始化生成好,ThreadX 在内核上挂两条线程,一条只管收、一条只管发,中断里只做三件事——搬字节、投信号量、退出。它适合两类人:被裸机大循环里的串口协议折腾过、想看看 RTOS 下该怎么分层的人;以及手上已有 ThreadX 工程、但串口驱动是抄来的、不确定对不对的人。工程本身不大,跑通一次半小时,但几个参数的取舍值得单独拎出来讲。

2. 工程骨架拆解:CubeMX 生成的初始化跟 ThreadX 内核怎么接上

拿到压缩包先别急着编译,先看目录。很多人直接双击 .ioc 重新生成一遍,结果自己加的两百行代码全被覆盖,这是最常见的一次意外状况。先把「哪些文件是 CubeMX 的、哪些是自己的」分清楚,后面所有改动才有依据。

2.1 目录结构:Core、AZRTOS、Middlewares 三块各管什么

打开工程根目录,能看到这么几个平行的文件夹,职责划分其实很清楚:

路径内容改动频率
Core/Src/main.cHAL_Init、时钟配置、外设初始化、最后调MX_ThreadX_Init低
Core/Src/app_threadx.ctx_application_define,创建线程、信号量、互斥量高
Core/Src/uart_port.c环形缓冲、DMA 重新挂载、收发 API高
Core/Src/stm32xxxx_it.c中断向量,最终调到 HAL 的弱回调低
Core/Inc/tx_user.hThreadX 编译期配置,tick 频率、错误检查开关中
AZRTOS/AppST 的封装层,池大小宏一般在这里中
Middlewares/ST/threadx内核源码 + 端口层汇编基本不动
Drivers/HAL 库与 CMSIS不改
UART_ThreadX.iocCubeMX 工程文件中

Middlewares/ST/threadx下面还会分成common/和ports/两块,common是跟架构无关的内核源码,ports下按内核分目录,Cortex-M4 和 Cortex-M7 各有一份tx_initialize_low_level.S。这份汇编是整个移植里最不该碰、也最容易被误改的文件,它干的事情只有两件:把 SysTick 和 PendSV 的优先级设到最低(0xFF),以及把链接脚本里堆顶的地址记录到_tx_initialize_unused_memory。

提示:_tx_initialize_unused_memory这个符号决定了内核能用的内存起点。如果你在链接脚本里改了 RAM 分区,却不改这个符号,表现出来就是内核启动正常、创建第二个线程就返回TX_NO_MEMORY,非常不易定位。

真正需要你维护的是app_threadx.c和uart_port.c这两个文件。我的习惯是:app_threadx.c只放内核对象的创建和线程入口函数体的骨架,具体业务逻辑全部放到独立模块里,app_threadx.c里只留一行调用。这样 CubeMX 重新生成时,就算app_threadx.c被覆盖,重新补十几个创建函数就行,业务代码一行不丢。

2.2 启动链:从 Reset_Handler 到 tx_kernel_enter 的七步

main.c在带 ThreadX 的工程里反而是最短的一个文件,因为它没什么可写的——走到最后一行就交权了:

int main(void) { HAL_Init(); /* 1. NVIC 优先级分组、HAL 时基源 */ SystemClock_Config(); /* 2. 主频、PLL、外设时钟使能 */ MX_GPIO_Init(); /* 3. 调试口、状态灯 */ MX_DMA_Init(); /* 4. DMA 控制器时钟与 NVIC */ MX_USART1_UART_Init(); /* 5. 串口参数,此时中断还没开 */ MX_ThreadX_Init(); /* 6. 进内核,这个函数不返回 */ while (1) { } /* 7. 永远走不到这里 */ }

逐行说一下这七步里容易忽略的地方。第 1 步的HAL_Init()会调HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4),也就是 4 位全给抢占优先级、没有子优先级——这个设置必须和 ThreadX 端口层的假设一致,中途改成分组 2 会让tx_thread_preemption_change的行为变得不可预期。第 5 步的MX_USART1_UART_Init()只配了波特率、字长、停止位,并不打开 NVIC 中断,真正打开是在HAL_UART_MspInit()里调HAL_NVIC_EnableIRQ(USART1_IRQn),这一步的时机很关键,后面避坑那章会专门讲。

第 6 步进到tx_kernel_enter(),内部依次是:_tx_initialize_low_level设置 SysTick/PendSV 优先级并把堆顶地址记下来,_tx_initialize_high_level初始化定时器、线程、队列等子系统,然后回调你的tx_application_define(first_unused_memory),最后启动调度器,从就绪链表里挑最高优先级的线程开始跑。

/* app_threadx.c —— 这段跑在调度器启动之前,中断仍然是关的 */ TX_THREAD g_rx_thread; TX_THREAD g_tx_thread; TX_SEMAPHORE g_uart_rx_sem; TX_MUTEX g_uart_tx_mutex; /* 栈用静态数组,比从 byte pool 里切更可控 */ static UCHAR s_rx_stack[1024] __attribute__((aligned(8))); static UCHAR s_tx_stack[1024] __attribute__((aligned(8))); VOID tx_application_define(VOID *first_unused_memory) { /* 用不到动态分配的话,first_unused_memory 可以不接 */ (void)first_unused_memory; tx_semaphore_create(&g_uart_rx_sem, "uart rx", 0); tx_mutex_create(&g_uart_tx_mutex, "uart tx", TX_INHERIT); tx_thread_create(&g_rx_thread, "uart rx", uart_rx_thread_entry, 0, s_rx_stack, sizeof(s_rx_stack), 8, 8, TX_NO_TIME_SLICE, TX_AUTO_START); tx_thread_create(&g_tx_thread, "uart tx", uart_tx_thread_entry, 0, s_tx_stack, sizeof(s_tx_stack), 10, 10, TX_NO_TIME_SLICE, TX_AUTO_START); }

tx_thread_create的那几个数字要解释一下:第 6、7 个参数是优先级和抢占阈值,ThreadX 里数值越小优先级越高,这里是 8 和 10,收线程优先级高于发线程,因为收方向丢一个字节就是整帧重传,发方向晚几毫秒没人看得出来。第 5 个参数是栈大小,单位是字节不是字,1024就是 1KB,这个数字给小了是硬伤,串口打印一句printf就可能吃掉 600 字节。

最后两个参数TX_NO_TIME_SLICE和TX_AUTO_START:前者表示同优先级之间不做时间片轮转,后者表示创建完立刻进就绪队列。如果你希望两个同优先级的线程公平轮流跑,把TX_NO_TIME_SLICE换成TX_1_TICK之类的时间片值。

2.3 CubeMX 里必须动的三个参数

生成的默认配置能编译能跑,但跑不长。下面这三处是我每建一个 ThreadX 工程都要回去确认的:

参数位置默认值建议值原因
Timebase SourceSYS → Timebase SourceSysTickTIM6 或 TIM7ThreadX 独占 SysTick 做内核 tick
USART1 全局中断优先级NVIC → USART1 global interrupt05 ~ 6数值 0 会抢在内核 tick 之前
TX_TIMER_TICKS_PER_SECONDtx_user.h1001000100 时tx_thread_sleep(1)等于 10ms,粒度太粗

第一项是最容易被忽略的。CubeMX 默认让 HAL 用 SysTick 做HAL_GetTick()的时基,而 ThreadX 的 Cortex-M 端口层也要把 SysTick 设成自己的 tick 源,两个东西抢同一个定时器。表现是HAL_Delay和tx_thread_sleep互相打架,时序全乱。在 CubeMX 的 SYS 页面把 Timebase Source 切到 TIM6,HAL 就会用 TIM6 的更新中断去喂uwTick,两边各用各的。

第二项的原理是:SysTick 和 PendSV 在端口层被设成最低优先级 0xFF,如果你的串口中断优先级设成 0,它就能打断内核任何一段临界区。虽然 ThreadX 在 Cortex-M 上默认用CPSID i关全局中断来做临界区保护,理论上不会被真正的临界区打断,但优先级 0 的串口中断会拖长中断响应嵌套链,高波特率下表现为偶发的字节错位。设成 5~6 是个折中:比 tick 高,保证串口能及时取走数据;又不会压过所有系统中断。

第三项直接决定tx_thread_sleep的精度。tx_thread_sleep(1)在 100Hz 配置下是 10ms,在 1000Hz 下是 1ms。串口超时判断一般按毫秒算,用 100Hz 会让所有超时都变成一个数量级的粗粒度,调起来很别扭。

3. 串口驱动核心:中断搬字节,信号量交数据,互斥量守通道

分层原则一句话说完:中断里不做任何耗时超过几微秒的事,线程里不做任何不让出 CPU 的等待。这份工程的uart_port.c就是围绕这两条搭起来的。

3.1 中断回调里的三行代码:为什么不能就地解析协议

先看接收侧。用 DMA + 空闲中断是这份工程的选择,比单字节中断省太多 CPU:

/* uart_port.c —— 接收侧全局变量 */ #define UART_RING_SIZE 1024u /* 必须是 2 的幂,用掩码代替取模 */ #define UART_DMA_CHUNK 64u static uint8_t s_ring[UART_RING_SIZE]; static volatile uint16_t s_head; /* 只有中断写 */ static volatile uint16_t s_tail; /* 只有线程写 */ static volatile uint32_t s_drop_cnt; /* 溢出计数,调试时看这个 */ static uint8_t s_dma_buf[UART_DMA_CHUNK]; /* HAL 的空闲中断回调,运行在中断上下文 */ void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart->Instance != USART1) { return; } for (uint16_t i = 0; i < Size; i++) { uint16_t next = (uint16_t)((s_head + 1u) & (UART_RING_SIZE - 1u)); if (next == s_tail) { /* 满了,丢新字节,保住老数据 */ s_drop_cnt++; continue; } s_ring[s_head] = s_dma_buf[i]; s_head = next; } tx_semaphore_put(&g_uart_rx_sem); /* 不能带超时,中断里不许阻塞 */ }

逻辑上分三段。第一段是一次最多 64 字节的 DMA 搬运,Size参数由 HAL 根据 DMA 剩余计数算出来,是本次实际收到的字节数,不是缓冲区长度。第二段是写环形缓冲,用(head + 1) & (SIZE - 1)求下一个位置,UART_RING_SIZE取 2 的幂就是为了省掉一次除法——Cortex-M4 上一条UDIV是 2~12 个周期,在中断里能省则省。第三段只投一个信号量,唤醒接收线程。判断满的方式是「下一个 head 撞上 tail」,牺牲一个字节的空间换取无锁的读写分离。

s_drop_cnt这个变量不起眼但很有用。串口不通的时候,先看它是 0 还是猛涨:一直是 0 说明数据根本没进来,问题在物理层或 DMA 配置;猛涨说明线程侧消化太慢,得调大缓冲或者提高线程优先级。这就是现场排查时最实用的指标。

参数上有一点要提醒:HAL_UARTEx_RxEventCallback是 HAL 在HAL_UART_IRQHandler里根据 DMA 状态调的回调,它同时承载「半传输」和「空闲」两种事件。半传输事件默认是开的,意味着传满 32 字节就会进来一次,和空闲中断混在一起。如果不需要这么高频,可以在HAL_UART_MspInit里__HAL_DMA_DISABLE_IT(huart->hdmarx, DMA_IT_HT);把半传输关掉,只留空闲触发,中断次数能少一半。

3.2 接收线程:信号量 + 环形缓冲,把字节流拼成帧

线程侧的逻辑反而简单,因为所有的脏活都在中断里做完了:

/* 接收线程入口 */ static VOID uart_rx_thread_entry(ULONG arg) { uint8_t chunk[64]; for (;;) { /* 1. 一直等到中断投信号量,期间不占 CPU */ if (tx_semaphore_get(&g_uart_rx_sem, TX_WAIT_FOREVER) != TX_SUCCESS) { continue; /* 理论上不会走到,出错直接重来 */ } /* 2. 一次最多搬 64 字节出来,剩下的下一轮再说 */ uint16_t n = 0; while (n < sizeof(chunk) && s_tail != s_head) { chunk[n++] = s_ring[s_tail]; s_tail = (uint16_t)((s_tail + 1u) & (UART_RING_SIZE - 1u)); } if (n == 0) { continue; } /* 3. 解析放在这里,慢一点也不会丢后面的字节 */ proto_feed(chunk, n); } }

tx_semaphore_get的超时参数用TX_WAIT_FOREVER,是因为这条线程就是专门为串口服务的,没数据时挂起零开销,不需要超时兜底。如果你还想让这条线程周期性做点别的事(比如心跳),把超时改成 100 tick,超时返回TX_NO_INSTANCE时走另一条分支就行。

第 2 步的while有一个细节:只搬 64 字节,如果环形缓冲里积压了 500 字节,剩下的要等下一轮信号量。这样写的好处是单次处理时间可控,不会因为积压太多一次卡死几十毫秒;代价是解析函数要能处理「半帧」,不能假设一次proto_feed就拿到完整一帧。如果协议本身要求整帧进整帧出,把chunk开大到UART_RING_SIZE,一次搬完再解析。

proto_feed是留给二次开发的接口,默认实现里可以先只做一件事——找帧头帧尾、算校验、把完整帧投到命令队列。把「收字节」和「解析帧」分开,是这套骨架最有价值的约定。

3.3 发送侧:互斥量守住 huart1,DMA 发完再释放

多个线程往同一个串口写数据,在这个工程里是常态:一条线程回命令,一条线程打日志,偶尔还有一条线程做 OTA 心跳。不加保护的话,两个HAL_UART_Transmit交叉执行,输出就是两段字节互相插花,看起来像乱码,实际是竞态。

TX_SEMAPHORE g_uart_tx_done_sem; /* 发送完成回调,由 DMA 传输完成中断触发 */ void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { tx_semaphore_put(&g_uart_tx_done_sem); } } int uart_send(const uint8_t *buf, uint16_t len, ULONG timeout) { /* 1. 抢通道,抢不到说明别人正在发 */ if (tx_mutex_get(&g_uart_tx_mutex, timeout) != TX_SUCCESS) { return -1; } /* 2. 挂 DMA,注意 buf 在发完之前不能被释放或改写 */ if (HAL_UART_Transmit_DMA(&huart1, (uint8_t *)buf, len) != HAL_OK) { tx_mutex_put(&g_uart_tx_mutex); return -2; } /* 3. 等发完,超时说明链路有问题,别无限期挂住 */ UINT rc = tx_semaphore_get(&g_uart_tx_done_sem, timeout); tx_mutex_put(&g_uart_tx_mutex); return (rc == TX_SUCCESS) ? 0 : -3; }

tx_mutex_create时传的TX_INHERIT是关键参数,它开启优先级继承。假设低优先级的日志线程拿到了互斥量,正在等 DMA,此时高优先级的命令线程来抢,如果没开继承,命令线程会被中间优先级的线程无限期挡住,这就是典型的优先级反转。开了继承之后,内核会临时把持锁线程的优先级提到和等待者一样高,让它尽快跑完释放。

timeout参数我一般传TX_WAIT_FOREVER给必须发出去的数据(命令响应),传 100 tick 给可丢弃的数据(日志)。区分这一点很重要:日志发不出去就丢掉,命令响应丢了会导致上位机超时重传。

第 2 步的注释是个真坑。HAL_UART_Transmit_DMA只是把缓冲区地址交给 DMA 控制器就返回了,如果buf是栈上的临时数组,函数返回后这块栈被覆盖,DMA 还在往外搬旧数据,发出去就是一串垃圾。要么buf用静态区,要么像上面这样等发送完成再返回。

3.4 把「发什么」和「怎么发」解耦:命令队列

上面那个uart_send是同步的,调用者要等到发完才能继续。命令响应用它没问题,但如果日志量一大,每条日志都占着一个互斥量等 DMA,吞吐就上不去了。加一层队列就解决了:

TX_QUEUE g_uart_tx_queue; /* 消息大小 4 字节,存的是 buffer 索引 */ /* 日志线程只负责投递,不关心什么时候真正发出去 */ int log_post(const char *fmt, ...) { uint8_t *slot = bufpool_alloc(); /* 从静态缓冲池里拿一块 */ if (slot == NULL) { return -1; /* 池子空了,直接丢,不阻塞 */ } int n = vsnprintf((char *)slot, LOG_SLOT_SIZE, fmt, va_list_stuff); if (tx_queue_send(&g_uart_tx_queue, &slot, TX_NO_WAIT) != TX_SUCCESS) { bufpool_free(slot); return -1; } (void)n; return 0; }

tx_queue_send的最后一个参数在日志场景一定用TX_NO_WAIT。队列满了就丢,这是设计上的取舍——日志不该成为阻塞业务的原因。如果改成TX_WAIT_FOREVER,日志量一大,所有调log_post的线程全部卡在队列上,表现出来就是「系统莫名其妙变卡」,查半天才发现是打印拖的。

发送线程那边就是死循环tx_queue_receive+uart_send+bufpool_free。队列深度按最坏情况估,一般给 8~16 个槽位,每个槽位 128 字节,总共不到 2KB,比给日志线程开大栈划算得多。

4. 编译、烧录与验证:怎么确认线程真的在跑

代码写完只是开始,能不能证明它在按你想象的方式跑,才是关键。这一章讲的是怎么用最短路径确认调度、缓冲、中断三件事都正常。

4.1 编译选项与内存布局:先确认池子够不够

工程用arm-none-eabi-gcc+ CMake 或者 CubeIDE 都能编。不管哪种,编译完先看.map文件里三个段的占用:

arm-none-eabi-size build/UART_ThreadX.elf # text data bss dec hex filename # 48216 1124 18392 67732 10894 build/UART_ThreadX.elf

bss段是重点。所有静态线程栈、环形缓冲、日志槽位都落在这里,18392字节意味着 RAM 已经被吃掉 18KB。如果你的芯片只有 64KB RAM,加上 HAL 自己的缓冲和堆,余量就不多了。这时候要么裁缓冲,要么换更大 RAM 的型号,别硬撑。

如果用了 byte pool 做动态分配,tx_byte_pool_create的第三个参数(池大小)必须小于实际可用 RAM 减去 bss 之后的余量。宏定义在app_threadx.h里,CubeMX 重新生成可能会把它改回默认值,我一般在tx_user.h里覆盖一层,避免被改回去。判断依据很简单:池子开太大,tx_byte_pool_create会返回TX_SIZE_ERROR;开太小,跑到第二个线程就TX_NO_MEMORY。两个错误码方向相反,看返回值就能定位。

4.2 打三个断点,看调度器是不是真的在切

烧录进去,先别急着开串口助手。打三个断点,一个一个确认:

第一个断点打在tx_application_define的第一行。能停下来,说明_tx_initialize_low_level和内存初始化都过了。如果停不下来,十有八九是链接脚本里_tx_initialize_unused_memory符号没定义,或者tx_initialize_low_level.S没被加进编译列表——检查 CMakeLists 或者 CubeIDE 的源文件排除状态。

第二个断点打在uart_rx_thread_entry的for(;;)里面。全速跑,能停下来,说明调度器起来了、线程被选中了。如果一直不停,看tx_thread_create的返回值,或者用调试器的 Watch 窗口看g_rx_thread.tx_thread_state,值是TX_READY还是TX_SUSPENDED。

第三个断点打在HAL_UARTEx_RxEventCallback里。串口助手发一串数据,能停下来,说明 DMA 和空闲中断都通了。这一步停不下来最常见的原因是 NVIC 中断没使能,或者 DMA 通道配错了。

三个断点都能停下来,基本可以确定链路是通的。这套方法看着笨,但比盯着串口助手猜快得多。

4.3 黑盒验证:GPIO 翻转和串口回环

断点会打断实时性,验证吞吐的时候得换黑盒手段。两个办法最实用。

第一个是 GPIO 翻转。在接收线程进入和退出时各翻转一次空闲引脚:

/* 用示波器或逻辑分析仪看这个引脚,占空比反映 CPU 占用 */ #define PROF_ENTER() (GPIOC->BSRR = GPIO_BSRR_BS_6) #define PROF_EXIT() (GPIOC->BSRR = GPIO_BSRR_BR_6)

如果占空比一直在 80% 以上,说明接收线程处理太慢,得看s_drop_cnt是不是在涨;如果占空比很低但数据还是丢,问题就在中断侧而不是线程侧。这一招在排查「到底是收不进来还是处理不过来」的时候特别好用。

第二个是串口回环。把 TX 和 RX 短接,发什么应该收到什么。回环测试能一次性验证波特率、字长、停止位、DMA 配置这四件事,比逐项检查快。回环不通但断点能停,多半是波特率算错了——检查SystemClock_Config里给 USART1 的时钟源频率,HAL 计算 BRR 时用的就是这个值,主频改了但 CubeMX 没重新生成,就会出现「配置里写 115200、实际跑 9600」的情况。

5. 避坑与排查:五条踩过的记录

下面这五条都是这套骨架在不同项目里真实复现过的问题,按「现象 → 原因 → 解决」写,遇到类似的直接对号入座。

5.1 现象:串口只收到第一个字节,再发就没反应

原因:HAL_UART_Receive_IT是一次性的。回调进一次之后,接收状态机就回到HAL_UART_STATE_READY,但 NVIC 还开着,下一个字节进来只会置位错误标志,不会进回调。很多人以为 HAL 会自己续上,实际上是自己在回调里重新挂。

解决:在HAL_UARTEx_RxEventCallback或HAL_UART_RxCpltCallback的末尾,重新调用一次HAL_UARTEx_ReceiveToIdle_DMA(&huart1, buf, len),并且重新关掉半传输中断。更稳妥的做法是把「重新挂载」封成一个uart_rx_arm()函数,初始化时调一次,回调末尾调一次,只有一处逻辑。

5.2 现象:线程里调了 HAL_Delay 之后,整个系统像被冻住

原因:HAL_Delay是忙等,本质是while (uwTick - start < ms),全程占着 CPU 不释放。ThreadX 是抢占式的,但如果这条线程优先级最高,它忙等的时候低优先级的线程一次都跑不到。另一个隐藏原因是 HAL 时基还挂在 SysTick 上,和内核 tick 抢同一个定时器。

解决:线程上下文里一律用tx_thread_sleep(ms * TX_TIMER_TICKS_PER_SECOND / 1000),把 CPU 让出去。同时在 CubeMX 的 SYS 页面把 Timebase Source 改成 TIM6。改完之后用 4.3 那个 GPIO 翻转法复测,占空比应该明显下降。

5.3 现象:加了 printf 之后输出乱码,或者直接停在 HardFault_Handler

原因:三个独立问题叠在一起。一是 newlib 的_write没重定向,printf的输出被丢到默认的_write里,有的工具链实现会触发半主机异常。二是printf内部不是线程安全的,两条线程同时进来会踩同一块静态缓冲。三是printf的栈开销大,格式化浮点时能吃掉 600 字节以上,1KB 的线程栈直接溢出。

解决:重定向_write到HAL_UART_Transmit,外面套一层互斥量;或者干脆放弃printf,写一个基于 3.4 那个队列的log_post。栈至少给到 2KB,如果一定要格式化浮点,给 3KB。这属于典型的「为了省事结果更费事」。

5.4 现象:跑十几分钟到几小时随机死机,复位之后又能跑

原因:栈溢出或者动态分配失败没判返回值。tx_byte_allocate返回TX_NO_MEMORY时,指针还是 NULL,后面直接往里写就是踩内存。另一种是中断里调了带超时参数的服务,比如tx_semaphore_get(&sem, 100),在中断上下文里超时参数必须是无符号的零,传非零会返回TX_WAIT_ERROR并可能破坏内核状态。

解决:在tx_user.h里打开 `TX_ENABLE_STACK

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询