RTX 学习笔记
本帖最后由 myxiaonia 于 2014-7-9 10:24 编辑RTX看的差不多了,笔记基本还没开始写,就先把例程发上来吧,大家看看有没有什么错误,最好有个全部使用系统服务的例程可以完整的测试rtx
模拟器可以直接在mdk上仿真
更新例程,解决rtx版本不同造成的不兼容(RTL.H和RTX_lib.C使用的mdk自带版本,重新打包,将这俩文件也放回去,这样就和mdk版本无关了) 增加mdk515中动态内存管理函数和cm0操作系统相关层 例程有问题,编译后,运行5s,就死机了
yueqia1988 发表于 2014-7-8 16:41
例程有问题,编译后,运行5s,就死机了
哦,怎么样的错误,是选择的simu吗,mdk环境是什么,我用的是474,运行一天都没问题的 myxiaonia 发表于 2014-7-8 18:41
哦,怎么样的错误,是选择的simu吗,mdk环境是什么,我用的是474,运行一天都没问题的 ...
终于开动了,哈哈!期待! 我的用的是mdk4.23,运行5s,就死机
5s后就死在了os_error函数中 你在RTX中使能硬件中断怎么弄的 yueqia1988 发表于 2014-7-9 10:18
你在RTX中使能硬件中断怎么弄的
已经更新例程,现在和mdk版本无关了
要使用中断,在main函数开始部分初始化这些外设中断 如果启动RTX后开启中断,怎么弄
支持楼主{:lol:}{:lol:}{:victory:}{:victory:} kinsno 发表于 2014-7-9 08:20
终于开动了,哈哈!期待!
可以先看看例程啊嘿嘿,我在里面新建了svn的库,代码修改都有记录,有时间可以看看有没有什么修改的不对的地方
mdk的 SVCS菜单下集成了TortoiseSvn的命令,就是例程目录下的SVN.SVCS文件,装有Tortoisesvn的同学可以把这个库切换回你自己的目录 yueqia1988 发表于 2014-7-9 10:28
如果启动RTX后开启中断,怎么弄
建议查看rtx的帮助文档
中断通道可以在main函数开始部分就首先使能,具体的外设中断可以在任务中使能
stm32的外设寄存器空间可以在unprivilege模式下读写,cortex-m3的SCB空间不能在unprivilege模式下写(读是可以的,建议看我的有关帖子),因此在RTX启动前配置SCB的寄存器
在任务函数中也可以写SCB,需要使用你自定义的svc中断函数 Eric2013 发表于 2014-7-9 10:29
支持楼主
ERIC2013大侠,多多指教啊呵呵,帮忙看看改的有没有什么bug啊 恩,但是使用svc中断函数,时间有点长 yueqia1988 发表于 2014-7-9 10:37
恩,但是使用svc中断函数,时间有点长
可以在rtx-config.c文件中配置成priviledge模式,何时何地都可以读写scb myxiaonia 发表于 2014-7-9 10:44
可以在rtx-config.c文件中配置成priviledge模式,何时何地都可以读写scb
是,这个就是程序的运行的安全性得不到保证了,只能使用svc了, myxiaonia 发表于 2014-7-9 10:37
ERIC2013大侠,多多指教啊呵呵,帮忙看看改的有没有什么bug啊
好的 本帖最后由 myxiaonia 于 2014-7-11 13:44 编辑
这里讨论的RTX是cortex-m系列版本,严格的说是CM3系列,M0和M4系列貌似就一个硬件相关文件的差别,有兴趣的坛友可以尝试分析下。
研读RTX的基本思路是根据RTX内核相关度区分,从相关度较低的文件入手,从低到高一个个理解。用这样的思路基于以下理由:通用的知识理解起来较为容易,毕竟有其他的帮助文档可以参考;专用的知识必须在明白其意图后才能还原,如果此时还在纠结通用知识点问题就比较麻烦了。
HAl_CM3.c,这个是硬件相关部分,和内核相关较小,cm3的参考文档基本够用;中间有些小插曲,此处按下不表。
rt_MemBox.c,其实就是固定大小内存管理,这个和标准库的Heap管理有很大不同。
线程间同步和通信功能,包括rt_Event.c,rt_Semaphore.c,rt_MailBox.c和rt_Mutex.c这几个文件。
Timer.c是个内核无关的组件,用于扩展用户自定义定时器,用的人好像不多,放在这里的原因是其管理多个用户定时器的思路是和延时操作一致的。
Time.c比较简单,提供了任务延时接口;Robin.c也比较简单,核心思路和区间延时差不多,两者的主要实现方法都在链表操作集rt_List.c中
rt_List.c是内核数据结构的主要形式,RTX管理任务就绪表、同步量等待表和任务延时表都是使用的链表完成,遍历链表也没有使用什么高级算法,简洁的同时可能在任务较多时性能会打折扣。
rt_Task.c主要提供有关任务新建删除等接口,是和内核调度器沟通的桥梁,rt_System.c则是内核其他特性的一些接口
接下首先看看HAl_CM3.c和MeBox.c这两个文件,对于此部分已经熟悉过的坛友们可以略过看下文
HAl_CM3.c
_alloc_box 和 _free_box
两者是rt_MemBox.c中对应函数的封皮函数,个人看来没有必要,而且其实现也是值得商榷的,我曾经在坛里发帖讨论过此问题。试讨论下_alloc_box
__asm void *_alloc_box (void *box_mem) {
/* Function wrapper for Unprivileged/Privileged mode. */
LDR R12,=__cpp(rt_alloc_box)
MRS R3,IPSR
LSLS R3,#24
BXNE R12 ;此处表明在中断中执行此函数的话直接调用rt_alloc_box
MRS R3,CONTROL
LSLS R3,#31
BXEQ R12 ;此处表明若内核运行在Privileged的话直接调用rt_alloc_box
SVC 0
BX LR ;否则只好在SVC中断中执行rt_alloc_box
ALIGN
}
以上注释,唯一一个共同点就是rt_alloc_box必须在Privileged模型下运行,因为CM3在中断中自动运行在Privileged模式。在我看来毫无必要,Unprivileged/Privileged除掉对SCB空间读写限制外,就是对MPU空间的读写限制;如果alloc和free需要在Privileged模式运行,那么对于alloc分配的内存要想使用的话是不是也要在Privileged模式运行,自己用SVC中断来实现?果真是为兼容的话,大多数情况下我看没必要用这个封皮嘛!!!
还有种情况,rt_alloc_box和rt_free_box的实现在CM系列中是使用互斥访问指令实现,而非CM系列则是用全局关中断实现的,这大概是需要Privileged模式支持的原因,可惜CM系列并不如此;个人建议不需要使用这两个封皮
SVC_Handler
这个就是SVC中断函数了,是RTX内核服务的核心之一,有关CM3的SVC中断函数写法在参考文档中十分详细,不再赘述。此函数实现两个功能,如果是用户自定义SVC函数,解析SVC中断号并提取对应函数,可以看成软件级别的向量中断;否则就是内核SVC服务了,RTX使用SVC 0作为内核系统调用接口,用R12传递内核服务函数(__svc_indirect),貌似DOS也有类似做法;运行内核服务函数完毕后,RTX立刻进行实际的任务切换,上下文保存和恢复,如果删除任务则不保存此任务现场(实际上此时任务栈已经释放)。SVC任务切换还有个重要的概念就是返回值更新问题,所有导致任务被切换的内核服务(任务被阻塞),都被标记为更新返回值,原因是线程间同步和通信服务一般都有个超时选项,任务切合时就至少有两种可能,要么超时要么成功。
PendSV_Handler
大名鼎鼎的PendSV,一般的RTOS可能不用SVC,但是不用PENDSV的基本没有。几乎其他所有的RTOS都使用临界区保护的方法,一般都是全局关中断操作,但是RTX最特殊的一点,其不需要在内核服务中进行关中断操作;PENDSV是为中断调用内核服务准备的,任务级调用内核服务则使用SVC完成,利用SVC+PENDSV这对漂亮的组合,RTX实现了这一独树一帜的特性(不过呢,对于没有SVC+PENDSV中断的硬件环境,也是可以实现这种做法的,这实际上代表一种思路)。从这一点上来说,RTX是一个和CM系列高度耦合的,是一个量身定制,真正体现CM系列优势的RTOS。RTX中同样也在PENDSV中断中进行任务切换,这个和SVC中一样的做法,PENDSV通过调用rt_pop_req,遍历OSFIFO的方式,在此实际完成中断内核服务调用。RTX提供一些中断内核服务调用接口,这些接口实际上只是将一个内核服务调用通过巧妙的互斥访问操作放到OSFIFO中,从而保证即使这些中断发生嵌套,放入OSFIFO的操作都不会出错,而OSFIFO使用出对入队两个独立的指示器和互斥访问,PENDSV一样也可以被中断调用安全的抢占。
SysTick_Handler
SysTick实现了系统TICK,为RTX实现和时间有关的内核服务提供条件。其通过调用rt_systick完成任务延时处理,用户定时器处理和任务时间片轮转功能,之后就是实际的任务切换。
OS_Tick_Handler
在CM系列中,一般不用此中断;根据RTX相关文档,此中断是为某些双核MCU准备的,例如NXP的LPC4357这种M0核没有实现SYSTICK。
SVC+PendSV+SysTick,是RTX内核管理的仅有的3个中断,其实现中将PendSV+SysTick定为最低优先级,SVC定位为比其高一个优先级并且至少能抢占,我通过代码分析,理解其思路后认为,完全可以将SVC的中断优先级等同于其余两者,这个我在坛里也有讨论,理由如下:SVC只由任务中显式调用产生,因此其就算和PENDSV碰面时,也是因为中断抢占了SVC并挂起PENDSV的缘故,将先返回到SVC再执行PENDSV;SysTick如果和SVC同时发生,也将先轮到SVC;SVC执行期间,PendSV+SysTick没有任何机会(中断号自然顺序),而PendSV+SysTick执行时,根本不可能执行到SVC指令。
os_set_env
这个函数看其注释/* Switch to Unprivileged/Privileged Thread mode, use PSP. */就明白其用途,此函数运行后就进入RTX_CONFIG.c中设定的Unprivileged/Privileged模式,同时使用PSP指针,这个函数在启动RTX时内部调用。
rt_init_stack
这个函数用于初始化任务栈,有关栈大小和栈8字节对齐,以及用于栈检查的栈底幻数。有意思的是它模拟出好像此任务之前被保存过上下文的样子,初始化了16个寄存器入栈的值。任务创建时传递的参数被放在入栈时R0位置,任务首次被调度到时R0保存了这个传递的参数,暂时还不知道这个参数怎么用(想用的话还得用内联汇编显式调用R0???)。注意真实的任务SP值是保存在TCB中,不可能将SP值本身放入自己的栈中;我将初始化R4-R11部分删除了,因为任务被首次调度回时不会使用到这些寄存器。这个函数也在启动RTX时内部调用。
dbg_init、dbg_task_notify和dbg_task_switch
这几个函数通过ITM_PORT31传递有关任务切换信息,如果没有开启DBG_MSG这个宏,MDK就不能指示任务切换信息(Event Viewer),这貌似可以作为RTX在其他IDE,例如IAR上的os插件接口;不过System and Thread Viewer却不受影响
HAl_CM3.c的解析到此就结束了,肯定还有很多未到位的地方,理解错误的知识点也在所难免。对RTX有研究的坛友可以谈谈你们的看法
这中间还有很多小插曲,例如对于ALIGN这个汇编指示器的误解,呵呵,基本功还要加强啊
现在只能在 RTC_Conf_CM.c 中配置任务堆栈大小。请问能不能单独配置 某一个任务的堆栈大小? 太阳花 发表于 2014-7-11 15:22
现在只能在 RTC_Conf_CM.c 中配置任务堆栈大小。请问能不能单独配置 某一个任务的堆栈大小? ...
当然可以,在RTX_CONGFIG.c中配置Number of tasks with user-provided stack,自定义任务栈数量,然后在文件中定义一个你需要大小的数组作为栈空间,选择os_tsk_create_user作为你的任务初始化函数,把数组地址和大小传递进去即可 谢谢回去试试。
也就是说初始化的时候 ,不使用 这个函数osThreadCreate (osThread(DC_AC_Task), NULL); // create threads
而是使用 os_tsk_create_user。 持续关注啊,谢谢楼主! 本帖最后由 myxiaonia 于 2014-7-11 21:51 编辑
太阳花 发表于 2014-7-11 17:35
谢谢回去试试。
也就是说初始化的时候 ,不使用 这个函数osThreadCreate (osThread(DC_AC_Task), NULL ...
你用的是cmsis-rtx啊,不一样的,虽说cmsis-rtx是在rtx上加了层皮,不过基本看不到rtx的影子了,待我之后好好看下这个cmsis lvyi913 发表于 2014-7-11 19:23
持续关注啊,谢谢楼主!
还请多多提出意见呢,我也是初学呢呵呵 rt_MemBox.c
此文件完成固定大小内存块管理功能,包括_init_box、rt_alloc_box和rt_free_box这几个基本功能,RTX帮助文档特意注明alloc和free功能是可重入的(reentrant),当然也是线程安全的,标准库的Heap管理也许可以通过互斥量保证线程安全,不过其不可重入。试分析rt_free_box,看看其是如何实现可重入的
int rt_free_box (void *box_mem, void *box) {
/* Free a memory block, returns 0 if OK, 1 if box does not belong to box_mem */
...
if (box < box_mem || box >= ((P_BM) box_mem)->end) {
return (1);
}
...
do {
*((void **)box) = (void *)__ldrex(&((P_BM) box_mem)->free); //ldrex后,cm3首先记住了需要互斥写的内存地址
} while (__strex ((U32)box, &((P_BM) box_mem)->free)); //strex写操作,若发现以上内存地址值被改写,写入被忽略,并返回1重复以上过程
return (0);
}
CM3权威指南建议用这样的do-while对进行互斥访问,并且代码段应尽量短小,是不是很像自旋锁操作,这里再啰嗦下,像lpc4357这种多核mcu访问共享ram,靠关中断这种进入临界区保护的做法是无效的,必须是这种自旋锁形式的do-while才是真正安全可行的。上述代码在编译后产生反汇编序列类似于ldrex...str...strex...,这里中间有个STR操作,对于某些实现为简单保护整段内存地址的mcu,这里的STREX是无效的,代码会永远死在这个循环里;不过在隔壁坛里的一个帖子中,香水城版本亲自确认stm32只保护LDREX时的一个字的地址区间,所以在STM32中此代码有效。由此实现了将box这个值互斥的写入到((P_BM) box_mem)->free,而box地址上将跟随其成功写入((P_BM) box_mem)->free的值。从其代码来看,rt_free_box可以多处重复调用,甚至递归调用,也不会得到不可预期的结果,同样其也不会造成互斥量导致的任务死锁,这就保证了其可重入性
接下来简要提下membox管理固定大小内存块的思路,ucos2也用到了相同的方法,可见真正精炼高效的算法具有的旺盛生命力
membox就是个单链表,不同之处是这个单链表不需要下个节点指针,下个节点信息保存在内存块的首地址上,它能这么做的原因是空闲内存块链表是不保存任何有用信息的,正好可以利用成下个节点指针,已分配节点则不属于这个空闲链表,它可以保存任何有用信息,回收到空闲链表的节点则因其保存的信息已经失效,利用为下个节点指针也没有副作用。_init_box就是将一大块内存按照大小分割后,初始化成一个空闲链表的过程,rt_alloc_box就是从空闲链表头部获取一个内存节点,rt_free_box就是将一个内存节点回收到空闲链表的头部
rt_MemBox.c就是互斥访问和无下个节点指针的单链表这两个特别之处,分析到此结束 打算写事件标志、信号量、互斥量和邮箱这些线程间同步模块了,不知道该从哪里入手了呢,是写代码分析呢还是讲讲原理,原理也是讲烂了的知识点,没啥新意啊 本帖最后由 myxiaonia 于 2014-7-14 08:24 编辑
rt_Event.c,rt_Semaphore.c,rt_MailBox.c和rt_Mutex.c
在开始理解线程间同步模块前,有必要理解下其数据结构,从其类型定义入手,我们可以管窥出实现思路。
事件标志由于其为线程私有,与其他几个同步量的公共特性有明显区别,正是通过私有和公共的同步模块相互协调,RTX体现出极为灵活的特性。看看TCB任务控制块如何组织
typedef struct OS_TCB {
/* General part: identical for all implementations. */
U8 cb_type; /* Control Block Type */ //内存块ID号,RTX链表操作非常灵活性,传递参数时多用OS_XCB类型指针
U8 state; /* Task state */ //任务状态,不必多说
U8 prio; /* Execution priority */ //优先级,RTX的互斥量使用优先级继承,此为继承后优先级
U8 task_id; /* Task ID value for optimized TCB access */ //通过TID快速访问TCB,所有激活任务都注册到任务数组中,TID就是下标
struct OS_TCB *p_lnk; /* Link pointer for ready/sem. wait list */ //互斥量、信号量、邮箱等待双向链表中下个节点指针;就绪任务单链表下个节点指针;内存块中偏移量4
struct OS_TCB *p_rlnk; /* Link pointer for sem./mbx lst backwards*/ //双向链表中的上个节点指针
struct OS_TCB *p_dlnk; /* Link pointer for delay list */ //延时任务双向链表,下个节点指针
struct OS_TCB *p_blnk; /* Link pointer for delay list backwards */ //上个节点指针
U16 delta_time; /* Time until time out */ //任务延时时,保存延时时间;区间延时结束后,保存下个区间的时间戳,区间延时只需要初始化一次
U16 interval_time; /* Time interval for periodic waits */ //区间延时,为周期性延时用
U16 events; /* Event flags */ //TCB的事件标志,每个任务有16个事件标志
U16 waits; /* Wait flags */ //等待事件被阻塞时,暂存此处;事件或等待成功时保存实际等待到的事件
void **msg; /* Direct message passing when task waits */ //邮箱操作阻塞时,暂存接收或发送消息的地址
struct OS_MUCB *p_mlnk; /* Link pointer for mutex owner list */ //任务拥有的互斥量链表头节点
U8 prio_base; /* Base priority */ //基本优先级,未继承前的优先级
U8 ret_val; /* Return value upon completion of a wait */ //任务被同步量阻塞时,同时进入同步量等待链表和延时等待链表,此处指示应用程序从哪个等待链表返回
/* Hardware dependant part: specific for CM processor */
U8 ret_upd; /* Updated return value */ //返回值更新标记,替换R0值为ret_val
U16 priv_stack; /* Private stack size, 0= system assigned */ //仅在任务初始化时指示是否使用自定义栈,完全可以在任务初始化完成后用作其他用途。。。或者用其他变量传递这个参数么。。。
U32 tsk_stack; /* Current task Stack pointer (R13) */ //任务SP保存在此处
U32 *stack; /* Pointer to Task Stack memory block */ //任务栈的基址
/* Task entry point used for uVision debugger */
FUNCPptask; /* Task entry address */ //任务入口
} *P_TCB;
可以看出TCB非常紧凑,有些变量甚至还一值多用,RTX一个卖点就是资源占用同其他RTOS相比非常有优势;再看看OS_MCB、OS_SCB和OS_MUCB的结构
typedef struct OS_MCB {
U8 cb_type; /* Control Block Type */ //内存块ID
U8 isr_st; /* State flag variable for isr functions */ //没有深究,能看懂其意图,不能理解必要性
struct OS_TCB *p_lnk; /* Chain of tasks waiting for message */ //邮箱等待链表的头节点;内存块中偏移量4
U16 first; /* Index of the message list begin */ //邮箱FIFO中的入队指示器
U16 last; /* Index of the message list end */ //邮箱FIFO中的出队指示器
U16 count; /* Actual number of stored messages */ //FIFO中实际消息数量
U16 size; /* Maximum number of stored messages*/ //最大消息数量
void *msg; /* FIFO for Message pointers 1st element */ //消息数组的首个元素,声明为柔性数组代码会更漂亮;此为兼容性考虑(C99才支持柔性数组)
} *P_MCB;
typedef struct OS_SCB {
U8 cb_type; /* Control Block Type */ //内存块ID
U16 tokens; /* Semaphore tokens */ //信号量入口数(这个怎么翻译更合适)
struct OS_TCB *p_lnk; /* Chain of tasks waiting for tokens */ //信号量等待链表的头节点;内存块中偏移量4
} *P_SCB;
typedef struct OS_MUCB {
U8 cb_type; /* Control Block Type */ //内存块ID
U16 level; /* Call nesting level */ //递归互斥锁,释放和获取必须对等
struct OS_TCB *p_lnk; /* Chain of tasks waiting for mutex */ //互斥量等待链表的头节点;内存块中偏移量4
struct OS_TCB *owner; /* Mutex owner task */ //拥有此互斥量的TCB
struct OS_MUCB *p_mlnk; /* Chain of mutexes by owner task */ //任务拥有的互斥量链表下个节点(优先级继承用)
} *P_MUCB;
以上几个结构体中,都有p_lnk这个元素,并且偏移量均为4。原来RTX的链表操作具有高度重用性,一个链表操作集对应以上不同同步量链表操作,而性能损失却非常的小,RTX为追求内核极小的代码footprint真可谓费劲心机。请看以下OS_XCB结构体的形式
typedef struct OS_XCB {
U8 cb_type; /* Control Block Type */
struct OS_TCB *p_lnk; /* Link pointer for ready/sem. wait list */
struct OS_TCB *p_rlnk; /* Link pointer for sem./mbx lst backwards */
struct OS_TCB *p_dlnk; /* Link pointer for delay list */
struct OS_TCB *p_blnk; /* Link pointer for delay list backwards */
U16 delta_time; /* Time until time out */
} *P_XCB;
这个结构体保证了其中元素和OS_TCB、OS_MCB、OS_SCB和OS_MUCB中同名元素相同的偏移量,作为通用结构体来传递参数是最恰当不过了(不过任务就绪表头节点用此结构体浪费了14字节的内存)
通过以上分析,就可以猜出同步模块工作的大概步骤:事件标志模块通过tid快速对相应任务操作,当被阻塞就放入延时等待链表,被抢占时立即调度;邮箱模块管理一个FIFO,队列满时发送任务进入邮箱等待链表,队列空时接收任务进入邮箱等待链表,同时被阻塞任务也会放入延时等待链表(除非无限等待),这种内嵌的超时机制给应用程序编写带来极大便利;信号量模块设定一个入口数,释放入口的任务可能被正在等待入口的任务抢占,获取入口的任务则可能被阻塞到信号量等待链表和延时等待链表中;互斥量模块类似于1个入口的信号量,不过其增加了优先级继承内容,释放互斥量时还需恢复继承的优先级,获取互斥量时也需要进行优先级提升。
rt_Event.c和rt_Semaphore.c中看上去没有令人惊艳的内容,不再进行源码级分析;重点看看rt_MailBox.c和rt_Mutex.c的内容 rt_mut_wait是互斥量获取函数,此接口函数基本没有改动,思路非常清晰:互斥量能获取则获取,区分下首次获取还是递归获取;否则允许等待的话,加入到阻塞链表和延时链表,附带优先级继承。
OS_RESULT rt_mut_wait (OS_ID mutex, U16 timeout) {
/* Wait for a mutex, continue when mutex is free. */
P_MUCB p_MCB = mutex;
P_TCBprun = os_tsk.run;
P_TCB p;
if (p_MCB->level == 0) { //互斥量此时还是自由的
p_MCB->owner= prun; //获取此互斥量
p_MCB->p_mlnk = prun->p_mlnk; //加入到任务的互斥量链表
prun->p_mlnk = p_MCB;
goto inc;
}
if ((p = p_MCB->owner) == prun) { //互斥量已经为当前任务所有,递归调用
/* OK, running task is the owner of this mutex. */
inc:p_MCB->level++; //递归互斥锁加1
return (OS_R_OK);
}
/* Mutex owned by another task, wait until released. */
if (timeout == 0) { //无法获得互斥锁,又不允许等待,直接返回
return (OS_R_TMO);
}
/* Raise the owner task priority if lower than current priority. */
/* This priority inversion is called priority inheritance. */
if (p->prio < prun->prio) { //任务将阻塞到此互斥量等待链表,因此拥有者任务优先级继承
p->prio = prun->prio;
rt_resort_prio (p);
}
rt_put_prio ((P_XCB)p_MCB, prun); //按优先级插入到互斥量等待链表
rt_block(timeout, WAIT_MUT); //插入到延时任务链表
return (OS_R_TMO);
}
rt_mut_release是互斥量释放函数,这个函数进行了较大改动,之前坛里有发帖讨论,为引起重视起了个很大的标题,不过挖坑没成功,可能对RTX深入研究的坛友还是比较少;这里贴的是修改后代码,原代码主要改动的是二级链表删除、释放互斥量没有阻塞任务不进行调度、阻塞任务优先级较低不需恢复优先级继承和阻塞任务获取互斥量后直接和当前任务抢占,RTX代码有很多闪亮的地方,被我发现些可改进地方着实小兴奋一把。基本思路:参数检查,是否递归释放,否则真正释放此互斥量,首先删除拥有链表此互斥量节点,若此节点有阻塞任务,检查首个任务优先级并恢复优先级继承,节点任务获得互斥量进入就绪态并和当前任务抢占。
OS_RESULT rt_mut_release (OS_ID mutex) {
/* Release a mutex object */
P_MUCB p_MCB = mutex;
P_TCBp_TCB;
P_TCBprun = os_tsk.run;
if (p_MCB->level == 0 || p_MCB->owner != prun) { //不正确的释放
/* Unbalanced mutex release or task is not the owner */
return (OS_R_NOK);
}
if (--p_MCB->level != 0) { //递归释放,此时没返回表明互斥量可以真正释放了
return (OS_R_OK);
}
/* Remove mutex from task mutex owner list. */
//优化代码,互斥量链表中删除此互斥量,二级指针删除链表节点,linus大神讨论过这个问题
{
P_MUCB *p = &prun->p_mlnk; //原代码不精练,改之
do {
if (*p == p_MCB) {
*p = (*p)->p_mlnk;break;
}
else {
p = &(*p)->p_mlnk;
}
} while(*p);
}
if (p_MCB->p_lnk != NULL) {
/* Restore owner task's priority. */
//优化代码,只有在释放的互斥量阻塞任务优先级被继承,才需要恢复优先级 //原代码不精练,改之
if (p_MCB->p_lnk->prio >= prun->prio_base) {
U8 prio = prun->prio_base;
P_MUCBp_mlnk = prun->p_mlnk;
while (p_mlnk != NULL) {
if (p_mlnk->p_lnk != NULL && (p_mlnk->p_lnk->prio > prio)) {
/* A task with higher priority is waiting for mutex. */
prio = p_mlnk->p_lnk->prio;
}
p_mlnk = p_mlnk->p_mlnk;
}
prun->prio = prio;
}
/* A task is waiting for mutex. */
p_TCB = rt_get_first ((P_XCB)p_MCB); //获取互斥量阻塞链表头节点,此任务获得信号量
rt_rmv_dly (p_TCB); //同时从延时链表中移除
/* A waiting task becomes the owner of this mutex. */
p_MCB->level= 1; //首次获取时初始化
p_MCB->owner= p_TCB;
p_MCB->p_mlnk = p_TCB->p_mlnk;
p_TCB->p_mlnk = p_MCB;
p_TCB->ret_val = OS_R_MUT; //这两个任务都被阻塞过,更新返回值
prun->ret_val = OS_R_OK;
/* Priority inversion, check which task continues. */
rt_dispatch (p_TCB); //直接和当前任务抢占 原代码不精练,改之
}
return (OS_R_OK);
} myxiaonia 发表于 2014-7-12 15:45
rt_mut_wait是互斥量获取函数,此接口函数基本没有改动,思路非常清晰:互斥量能获取则获取,区分下首次获 ...
楼主位的工程里的代码也是带注释的?
你应该更倾向于原理性的描述和一些关键数据结构,最后对一些API提出一些常用用法,这样基本一个框架下来了,目的是让一个新手在最快的时间内可以使用它,源码极的我觉得可以放在第二步来讲或讨论或教学啊; kinsno 发表于 2014-7-12 16:58
楼主位的工程里的代码也是带注释的?
你应该更倾向于原理性的描述和一些关键数据结构,最后对一些API提 ...
例程基本没注释,你想看改动需要看修改记录,工程内建了tortoisesvn的版本库,在mdk的svcs中集成了操作集,重定位下库地址 今天重新看mailbox有关isr_st的内容,发现rtx的源代码还是有bug的:isr_st = 1时表明有发送任务被阻塞,但是rt_mbx_wait函数可能激活这些阻塞的发送任务,却没有对isr_st做任何修改,当多个rt_mbx_wait调用导致阻塞任务都被恢复,此时调用isr_mbx_receive就会isr_st = 2,错误的产生一次OSFIFO入队,不过rt_mbx_psh函数检查链表非空幸好没出错,内核多运行了无用功,万幸没出问题,但是糟糕的是程序并没因此结束,接下来当作isr_mbx_send引起的OSFIFO入队来看待了,导致严重错误;rt_mbx_psh在判断到isr_mbx_receive产生的(isr_st == 2)操作后,直接置isr_st = 0,不管是否还有阻塞的发送任务,这样再次调用isr_mbx_receive就不会激活这些阻塞任务,造成任务多余的等待,这个bug就比较糟糕了,影响内核的稳定性; 基本上没人关心啊,现在写下去的动力都不怎么有了。。。不知道哪个地方有讨论rtx这个rtos的 由于isr_mbx_send和isr_mbx_receive完全可能多次发生,多次嵌套,事情比我预想的还要复杂,仅仅靠isr_st值判断还是不够可靠啊
再加上我之前提出isr_mbx_receive只能在低优先级中断调用的问题
RTX的邮箱操作问题很严重啊,待我好好整理 接下来看看rt_MailBox的内容,之前谈到过,RTX中断调用内核服务的API使用的是OSFIFO入队的方式,不过isr_mbx_receive直接操作了邮箱对象,实际上破坏了安全的互斥访问特性,因此不是线程安全的。我研究的结果是必须限定调用isr_mbx_receive的中断优先级为最低。再有就是isr_st,经过研究,确认其各状态的意义:0-默认,邮箱没有阻塞的待发送任务,1-邮箱存在阻塞的发送任务,2-isr_mbx_receive释放了一个邮箱位置,允许阻塞的发送任务被唤醒并发送,不过RTX没有处理好这3种状态,引入了bug;同时rt_mbx_psh在区分到底是发送还是接收PUSH存在bug,可能引起RTX崩溃。邮箱就在这几个地方展开。
OS_RESULT isr_mbx_receive (OS_ID mailbox, void **message) {
/* Receive a message in the interrupt function. The interrupt function */
/* should not wait for a message since this would block the rtx os. */
P_MCB p_MCB = mailbox;
if (p_MCB->count) {
/* A message is available in the fifo buffer. */
rt_dec (&p_MCB->count); //邮箱对象写
*message = p_MCB->msg;
if (++p_MCB->last == p_MCB->size) { //未受保护的邮箱对象读写
p_MCB->last = 0;
}
if (p_MCB->isr_st == 1) {
/* A task is locked waiting to send message */
p_MCB->isr_st = 2;
rt_psq_enq (p_MCB, 0);
rt_psh_req ();
}
return (OS_R_MBX);
}
return (OS_R_OK);
}
isr_mbx_receive函数代码次序被我调整过,不影响功能。因其直接操作count、last和isr_st这3个邮箱对象又没使用互斥访问,因此其是不可重入,又因为在中断调用,连线程安全都无法保证。所以调用此函数中断不能嵌套RTX管理的3个中断,表明此中断也是最低优先级。再看看如下函数未修改前样子
void rt_mbx_psh (P_MCB p_CB, void *p_msg) {
/* Store the message to the mailbox queue or pass it to task directly. */
P_TCB p_TCB;
/* Check if this was an 'isr_mbx_receive ()' post service request. */
if (p_CB->p_lnk != NULL && p_CB->isr_st == 2) { //此处判断isr_mbx_receive推入的OSFIFO远远不够,isr_mbx_send和其背靠背多次发生后isr_st无法区分谁先谁后,需借助p_msg
/* A task is waiting to send message, remove it from the waiting list. */
p_CB->isr_st = 0; //直接置0,根本就不考虑是否还有等待发送任务被阻塞,再次调用isr_mbx_receive就无法将这些阻塞任务唤醒
p_TCB = rt_get_first ((P_XCB)p_CB);
p_TCB->ret_val = OS_R_OK;
/* Store the message to the mailbox queue. */
p_CB->msg = p_TCB->msg;
rt_inc (&p_CB->count);
if (++p_CB->first == p_CB->size) {
p_CB->first = 0;
}
goto rdy;
}
...
}
OS_RESULT rt_mbx_wait (OS_ID mailbox, void **message, U16 timeout) {
...
if (p_MCB->p_lnk != NULL) {
/* A task is waiting to send message */
if(p_MCB->p_lnk->p_lnk == NULL) { //原程序没有及时清0,可能给rt_mbx_psh带去错误的信号
p_MCB->isr_st = 0;
}
prun->ret_val = OS_R_OK;
p_TCB = rt_get_first ((P_XCB)p_MCB);
p_TCB->ret_val = OS_R_OK;
rt_rmv_dly (p_TCB);
rt_dispatch (p_TCB);
p_MCB->msg = p_TCB->msg;
if (++p_MCB->first == p_MCB->size) {
p_MCB->first = 0;
}
}
else {
rt_dec (&p_MCB->count);
}
...
}
邮箱部分主要修改的地方这是这3处。RTX邮箱被设计成可以容纳多个消息,允许任务阻塞的实例。从代码来看,RTX的邮箱允许同时有多个发送者和多个接收者的,不知道这样的邮箱怎么使用,多个接收者可能错误的收走了其它任务的消息,而本身却错失本属于自身的消息;现实中的邮箱应该有个主人,多人向其发送,只有它自己接收;也就是多发送者单接收者邮箱;RTX可能为兼容性考虑才如此设计。
时时更新例程,lz位已经不可编辑,请版主替换lz位的附件后删除此楼
本帖最后由 myxiaonia 于 2014-7-16 07:41 编辑
能删楼不 似乎有点曲高和寡,普通工程师最多是使用RTX,能够研究内核并能进一步发现bug的人已经很少了,能再进一步愿意将bug在坛里分享讨论的那更是少只又少了!
楼主,这注定是“一场孤独的旅行”! 希望你加油!! lvyi913 发表于 2014-7-16 08:55
似乎有点曲高和寡,普通工程师最多是使用RTX,能够研究内核并能进一步发现bug的人已经很少了,能再进一步愿 ...
以前研究UCOS2的很比较多,因为官方出过书,国内也有翻译版,学校里推荐学习的也多
这个rtx才开源没多久,网上几乎没有深入的分析,能够找到一份代码注释已经很不错了,当然rtx源码的英文注释已经讲得很清楚了 高手,请教在RTX的semaphore返回的是可用资源的token数,但如何知道具体哪个资源是可用的呢?token能不能返回目前具体可用的资源呢? cddx 发表于 2014-7-17 15:45
高手,请教在RTX的semaphore返回的是可用资源的token数,但如何知道具体哪个资源是可用的呢?token能不能 ...
当然只能返回token数量呢,具体的资源需要你自己维护,比如用数组值0或1表示资源可用,用下标来查询。
RTX里有个TID,就是任务表的下标,新建任务时首先看看这个任务表满不满,一样的思路 本帖最后由 cddx 于 2014-7-17 16:38 编辑
myxiaonia 发表于 2014-7-17 15:58
当然只能返回token数量呢,具体的资源需要你自己维护,比如用数组值0或1表示资源可用,用下标来查询。
RT ...
明白了,RTX的semaphore只是一个计数值,它也无法指明哪个资源可以。我用的是CMSIS-RTOS,但不知如何跟踪看源码,你给的示例还有点看不懂。请教一下如果用数组来指明资源的可用性,这段代码是在临界区吗?要用mutex保护吗?谢谢!
又看了一下源码,感觉semaphore返回的token代表可用资源是不是更有用,如0代表没有可用资源,1代表第一可用通道、、、但这要修改源码了。 cddx 发表于 2014-7-17 16:25
明白了,RTX的semaphore只是一个计数值,它也无法指明哪个资源可以。我用的是CMSIS-RTOS,但不知如何跟踪 ...
我也想到了这个临界区的问题,rtx有个tsk_lock/tsk_unlock这对临界区保护函数
tokens肯定不能作为具体某个资源可用的通道,例如以下情况,tokens全部被获取完,此时某个任务释放了一个tokens,不能说明就是第一通道被释放 myxiaonia 发表于 2014-7-18 07:39
我也想到了这个临界区的问题,rtx有个tsk_lock/tsk_unlock这对临界区保护函数
tokens肯定不能作为具体某 ...
如果获取和释放对应起来这个问题就可以解决,但可能内核维护变得复杂。 cddx 发表于 2014-7-18 10:49
如果获取和释放对应起来这个问题就可以解决,但可能内核维护变得复杂。 ...
主要是为通用性考虑,你的要求可能难以用通用的方式解决,这个可用资源数据用什么形式的数组来表示很难统一吧 本帖最后由 myxiaonia 于 2014-7-19 16:03 编辑
经过和MDK的员工沟通,确认以下事实:SVC必须可以抢占SYSTICK,原因是当两者几乎同时发生时,虽然SVC在NVIC中比SYSTICK排在前面,很有可能依旧先执行SYSTICK再执行SVC,SYSTICK切换任务的话就会导致SVC好像是另一个任务在调用的,上下文全变了,这是决不允许的。对NVIC的研究还是不够深啊呵呵,哪位之前知道这个知识点啊
这样的话,就要修改之前的读书笔记了,可惜我已经无法修改之前的楼层 我现在正在和MDK的员工讨论我提到的那些bug,有什么进展将在这里和大家一起分享,同时也希望RTX能够真正的被深入理解和广泛应用
不知道我绵薄之力能否对此推广有所助益 LZ 太棒了,我也在研究 RTX 的源码。坐等您的更新。 boddmg 发表于 2014-8-7 11:11
LZ 太棒了,我也在研究 RTX 的源码。坐等您的更新。
我都好久没更新了哈哈 主要是没人探讨啊 楼主强大,研究的很深入,
正在使用cmsis-rtos,关注楼主的分析。 MDK的员工?德国的? bigbadwolf 发表于 2014-8-22 15:25
MDK的员工?德国的?
不知道哪里的?是keil的,但是在arm网站记录过程的 支持楼主 写的好仔细啊 赞一个 现在的 RTOS 太多了,楼主能够专注RTX并分享研究心的,赞一个. 楼主这样的记录贴很好啊,赞 原来有个东西叫RTX,涨姿势了 貌似不错的说................ KEIL RTX RainKing 发表于 2014-9-4 19:21
貌似不错的说................
RTX是很不错的哦,尝试下会有好感觉 不错,收藏了 不错,收藏了 再更新一个内容,rtx可以带参数形式初始化一个任务,根据mdk官方一个手册说法,参数可以用于任务的多个实例,例如传递的是串口模块首地址 rtx cmsis-rtx 除了 keil MDK 外, 没有其他调试插件,相比 ucos 和 freertos 都有 IAR 调试插件. zf8848 发表于 2014-9-10 06:50
rtx cmsis-rtx 除了 keil MDK 外, 没有其他调试插件,相比 ucos 和 freertos 都有 IAR 调试插件. ...
实际上现在rtx已经开放代码,做这个插件已经没有障碍,临门一脚的问题 支持一下楼主,开始学习rtx了。 回去试试。 hsuda876 发表于 2014-9-15 14:09
支持一下楼主,开始学习rtx了。
祝你好运。。。有什么问题都可以发上来一起探讨 myxiaonia 发表于 2014-9-15 14:50
祝你好运。。。有什么问题都可以发上来一起探讨
{:lol:} 好的 myxiaonia 发表于 2014-9-15 14:50
祝你好运。。。有什么问题都可以发上来一起探讨
rtx是不是进中断不需要特殊处理?,我之前看rt rtthread的时候,进中断都会调用一个函数,出中断也会调用一个函数。看了rtx感觉很晕 hsuda876 发表于 2014-9-15 14:56
rtx是不是进中断不需要特殊处理?,我之前看rt rtthread的时候,进中断都会调用一个函数,出中断也会调用 ...
是的,不需要,只调用那些api即可,这也是rtx和其他类似rtos最大的不同,其他rtos由于追求通用性,一般都采用关中断-调用api-恢复中断这种做法,rtx充分利用了cortex-m系列的特点,详细解释你看看我楼上的笔记
关键字 SVC+PENSV
你也可以在坛子里搜搜rtx和其他rtos运行比较结果,基本完胜 myxiaonia 发表于 2014-9-15 15:00
是的,不需要,只调用那些api即可,这也是rtx和其他类似rtos最大的不同,其他rtos由于追求通用性,一般都 ...
哦,这个样子,有空仔细研读一下你的帖子。谢谢 hsuda876 发表于 2014-9-15 15:45
哦,这个样子,有空仔细研读一下你的帖子。谢谢
惊现永远不关中断和在ISR中上下文切换的RTOS
这个帖子提到了另外一个rtos,也是不需要关中断的,是iar自带Powerpac 楼主提到了一些rtx的bug,能不能单独的分析下哪些情况下可能会出现问题,最好针对每个bug给个example工程或者楼主提个思路大家把example工程做出来一起验证下。
这样可以容易的把bug复现出来,那么就可以有针对性的绕开或者去解决bug。不然空口说不好理解也不容易去复现,而且对于有些所谓的bug也可能是处于猜测中并不能完全肯定。
如果弄一些代码出来,即使最后发现不是bug也能把内核中可能引起误会的地方用代码的方式澄清下以免其他人读代码的时候遇到同样的问题
本帖最后由 myxiaonia 于 2014-9-20 12:57 编辑
我把rtx的源码放到网上svn空间了,大家在这个网址上下载源码吧
svn://www.oksvn.com/myRTX
需要使用TortoiseSVN,新建一个文件夹,用这个网站把代码检出(Check out)下来即可,有时间我会继续更新,也欢迎有兴趣的兄弟们一起看看
在mdk的scvs菜单下,可以对版本库进行一系列的操作 收藏,学习,谢谢! 有关RTX的返回值更新问题,在此做个小结
rtx函数返回值:nok-参数检查错误ok-同步块已经准备好,无阻塞完成(有可能在优先级较低的情况下被切出)tmo-超时,在限定时间内没有等到同步块 evt、sem、mbx、mut-在限定时间内等待同步块成功
后两者都真正地等待了同步块,和前两者有明显的区别
对照以上这条准则,可以发现RTX在处理这几个返回值上还是存在缺陷的
rt_evt_wait直接无阻塞等到事件时,返回了evt,应该为ok
rt_mbx_wait邮箱发送被阻塞函数,当获取邮箱消息后空出入口给此发送函数时,也应该返回mbx,原来用的是ok
isr_mbx_receive此函数因为不能被阻塞,因此可以认为是0时间等待(不等待)获取消息,两个返回值都需要修改
rt_mbx_psh同rt_mbx_wait类似的问题,原因是此前邮箱发送函数不会返回mbx,其实它也可能被阻塞嘛
我刚刚下载了MDK512并提前了其中的RTX源码,遗憾的是MDK5 中断返回更新 的做法被取消了,不知道是不是老早就取消了
个人觉得原来的做法给用户提供更多的消息,而且对于超时这种情况它到底怎么避免,还是说cmsis-rtx中的同步块系统API已经不支持超时做法,需要用户显式自定义,这不是自断手脚嘛。。。。 建议楼主做个文档出来,RTX还是不错的
PowerPAC 也就是 embos 的所谓零中断延迟是把中断分成高优先级和一般中断,高优先级中断不用 RTOS 函数,与不用RTOS系统时相比,没有中断延迟.
coOS 和 RTX 类似,也是这对 CM 优化, 内核不关中断的. 也就是说embos的0中断延迟和rtx的思路是不一样的喽,rtx是针对cm系列优化的,而embos是通用的但是也做到了0中断延迟,是这样吗 今天搜索RTX相关内容,竟然发现美国有家公司 IntervalZero 推出的产品也叫RTX,简单看了下介绍,这个产品也是个RTOS啊,真巧合啊哈哈哈。。。它的RTX是用在Windows上的,可以把Windows改造成为一个RTOS,很高级的一个应用 zf8848 发表于 2014-9-30 08:56
PowerPAC 也就是 embos 的所谓零中断延迟是把中断分成高优先级和一般中断,高优先级中断不用 RTOS 函数,与不 ...
我看你好像在你的产品上选择rtos啊,而且要求还挺苛刻。。。
请问你最后的评估结果是用的哪个啊,给大家分享一下实践经验 产品上曾经用过 MQX ,可惜有点臃肿,后来不用了,考察过 RTT,freertos,nuttx,rtx,ucos-ii/iii, embos, chibios,coOS等等,
如果不考虑扩展性,只是用于 CM 系列, 可以在 rtx,coOS 以及 freertos,chibios,rtt,nuttx选择 zf8848 发表于 2014-9-30 15:05
产品上曾经用过 MQX ,可惜有点臃肿,后来不用了,考察过 RTT,freertos,nuttx,rtx,ucos-ii/iii, embos, chibio ...
你有个帖子谈到你在做的高速串口和以太网网关呀,里面你不是要求高性能的rtos吗,最后你选择了哪个rtos啊 myxiaonia 发表于 2014-9-30 15:16
你有个帖子谈到你在做的高速串口和以太网网关呀,里面你不是要求高性能的rtos吗,最后你选择了哪个rtos啊 ...
这些RTOS都不符合当时的要求,裸奔的. 这个和ucos相比有什么优点? dzlt2012 发表于 2014-10-1 07:30
这个和ucos相比有什么优点?
这是keil专门针对arm优化设计的rtos,性能十分突出,楼上兄弟就有个比较各种rtos性能的帖子,可以请看看 myxiaonia 发表于 2014-10-1 13:39
这是keil专门针对arm优化设计的rtos,性能十分突出,楼上兄弟就有个比较各种rtos性能的帖子,可以请看看 ...
好的,我再深入了解一下。 我产品中也在用,没有考虑楼主那么深, 继续呀 RTX,学习, 楼主研究得非常深入,我也打算用RTX到产品中 McuPlayer 发表于 2014-10-31 14:48
楼主研究得非常深入,我也打算用RTX到产品中
好啊恭喜。。。有问题放上来一起探讨哦 keil 5.1.2 自带的 cmsis-rtos 版本可能更实用/可靠.
mbed rtos 就是基于 cmsis-rtos 的,虽然 arm 的 cmsis-rtos 就用 rtx 包装了一下. zf8848 发表于 2014-11-10 00:31
keil 5.1.2 自带的 cmsis-rtos 版本可能更实用/可靠.
mbed rtos 就是基于 cmsis-rtos 的,虽然 arm 的 cmsis ...
为何认为cmsis-rtx会更加可靠呢? myxiaonia 发表于 2014-11-10 07:32
为何认为cmsis-rtx会更加可靠呢?
从 rtx 发展的历程可以看出来,
在 cmsis-rtos 没有推出来之前, keil / arm 把 rtx 作为一个 rtos 的示例, 一直到 4.xx 都有一些长期 bug, 并且官方一直没有提供针对 iar 和 gcc 的支持.
但在 cmsis-rtos 推出来后, arm 为了推广 cmsis-rtos , 把 rtx 重新打磨, 提供了对 gcc 和 iar 的支持. 但在性能上一直是 arm 编译器最好, 比如arm 自家编译器支持 lock free , iar 和 gcc 都没有提供.当然自己移植也不难.
现在 arm 又开始 在 cmsis-rtos 基础上推广物联网 rtos 。
另外也可以比较 cmsis-rtos 不同版本的源代码, 可以看出 arm 在用心打磨 cmsis-rtos. zf8848 发表于 2014-11-10 07:48
从 rtx 发展的历程可以看出来,
在 cmsis-rtos 没有推出来之前, keil / arm 把 rtx 作为一个 rtos 的示例, ...
原生rtx有很多自己的特点,在cmsis-rtx上被抹去了,当然cmsis-rtx也有很多自己的特点。。。
我暂时只能先把rtx用熟悉了,而不仅仅是研究熟悉了,再去折腾cmsis-rtx,也许到时会有更多的理解和收获 myxiaonia 发表于 2014-11-10 08:00
原生rtx有很多自己的特点,在cmsis-rtx上被抹去了,当然cmsis-rtx也有很多自己的特点。。。
我暂时只能 ...
我也想用RTX啊,可是那多API都记不住啊,看来得去下载一个API手册了, 其实这种OS使用上都是大同小异,看API足矣吧;细节的话, 得深入去看了,肯定不是一朝半夕的,即便有人对所有的代码一行行的做解释,也无济于效,因为必须吸收理解;
我裁完了UCOS, 下一步来裁RTX啊,把RTX裁着只有3个功能, 延时,任务,邮箱;
本帖最后由 myxiaonia 于 2014-11-11 09:24 编辑
朝闻夕道 发表于 2014-11-10 08:11
我也想用RTX啊,可是那多API都记不住啊,看来得去下载一个API手册了, 其实这种OS使用上都是大同小异,看API ...
rtx手册就在mdk的帮助文档里面
还有,裁剪是没有必要的嘛,你不用这些功能,默认就没有这些代码。。。当然程序中还会保留这些功能的head,这会增加一些内存消耗但不大 希望楼主长期更新此贴,我对RTX也很有兴趣,最近在学习。 kevin_me 发表于 2014-12-2 09:51
希望楼主长期更新此贴,我对RTX也很有兴趣,最近在学习。
有问题讨论就更新,没有讨论不知道更新啥了 down下来36楼的工程之后,直接编译报错了。
提示没有stm32f10x.h头文件,我自行添加之后,编译报错缺少system_***.h 我忘了。
楼主这个都是需要自行添加的吗?
页:
[1]
2