传媒学子

  • 2019-05-20
  • 回复了主题帖: [GD32E231 DIY大赛] 04. 机械臂艰难的组装过程

    hujj 发表于 2019-5-19 20:54 看了几遍还没有明白这机械手的结构原理。
    很简单,四个舵机,转向,控制舵机就行

  • 回复了主题帖: [GD32E231 DIY大赛] 04. 机械臂艰难的组装过程

    dcexpert 发表于 2019-5-19 21:16 这种机械臂有两种,一种是较便宜的亚克力板做的,另一种是用金属结构件,使用舵机进行控制。舵机是50HZ的PW ...
    对对,我就是亚克力,采用的就是一点一点变化,变化大了,就掉电了。。。

  • 回复了主题帖: [GD32E231 DIY大赛] 03. 忽视运放设置会导致PB1输出电压不正确

    小涛电子 发表于 2019-5-19 23:03 231的运放怎么用 手册上好像资料不多 比如我要放大 输入脚和输出脚接电阻? 正反向输入脚为什么距离这么远 ...
    就当做正常的运放就行,貌似是轨至轨的,SPEC上有参数。 就是一个运放,怎么接都行

  • 2019-05-19
  • 发表了主题帖: [GD32E231 DIY大赛] 04. 机械臂艰难的组装过程

    本帖最后由 传媒学子 于 2019-5-19 18:29 编辑 [GD32E231 DIY大赛] 04. 机械臂艰难的组装过程 夹杂这省钱和锻炼自己动手能力的动机下,我掏了160多大洋买了一个四自由度的小型机械臂组装散件,本来以为比较组装起来比较简单,但可能是我太幼稚了,如果知道组装这么费劲,还不如直接多掏30多,买个成品,这里还是奉劝大家如果时间不是很充裕的话,还是买个成品的机械臂。毕竟我们是电子工程师,花费太多的时间在组装上,会影响心情的。 1.买来是这个样子的... 当时就有点抓狂了,没想到抓狂的事情还在后边。。。 #2. 初见框架# #3. 组装即将完成时,突然一根亚克力板断了。。。顿时懵逼了# 胶带缠了一下,凑合着用,后来商家又给发了一根。 #4.状况频出 一一击破# 组装完了,不会动?? 原来是螺丝拧的太紧了。 组装完了,动的异常?? 原来是电源功率不够。 花了近1个星期的闲暇时间,终于搞定了。。。 下一贴 分析如何用GD32产生PWM控制机械臂。

  • 发表了主题帖: [GD32E231 DIY大赛] 05. 自动喂鱼机器人之PWM篇

    [GD32E231 DIY大赛] 05. 自动喂鱼机器人之PWM篇 1.舵机基本工作原理 一般机器人控制都离不开舵机控制,这里先简单讲解一下舵机的基本工作原理: 舵机的控制信号为周期是 20ms 的脉宽调制(PWM)信号,其中脉冲宽度从 0.5ms-2.5ms,相对应舵盘的位置为 0-180 度,呈线性变化。也就是说,给它提供一定的脉宽,它的输出轴就会保持在一个相对应的角度上,无论外界转矩怎样改变,直到给它提供一个另外宽度的脉冲信号,它才会改变输出角度到新的对应的位置上。 一般而言,舵机的基准信号都是周期为20ms,宽度为1.5ms。这个基准信号定义的位置为中间位置。其中间位置的脉冲宽度是一定的,那就是1.5ms。 我是参考: https://blog.csdn.net/weixin_38075894/article/details/80027600. 20ms基准频率就是50Hz, 这个一般的单片机都是很容易生成的。 我的机械臂上的舵机型号是MG90S,是一个小舵机,机械臂是4自由度的,因此就是4个。 GD32E231的定时器功能是非常强大的,TIMER2可以同时输出四个通道的PWM,PWM可以设定不同的脉宽。 2.硬件配置 因为GD32E231start开发板,兼容arduino接口,因此我采用了arduino的转接板,转接板仅仅是转接功能,具体如下: 具体的接口配置: PB4 TIMER2_CH0 D3 PB5 TIMER2_CH1 D4 PB0 TIMER2_CH2 D9 PB1 TIMER2_CH3 D8 3程序如下: void gpio_config(void); void pwm_timer_clock_enable(void); void pwm_timer_config(uint32_t timer);复制代码 void gpio_config(void) { rcu_periph_clock_enable(RCU_GPIOB); /* configure PB2 output 0 */ gpio_mode_set(GPIOB, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_PIN_2); gpio_output_options_set(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ,GPIO_PIN_2); gpio_bit_reset(GPIOB, GPIO_PIN_2);//disable opa, ENA is connected to PB2, ENA = 0, diable opa, else, enable ENA, NOT be float /* configure PB4(TIMER2 CH0) as alternate function */ gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_4); gpio_output_options_set(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ,GPIO_PIN_4); gpio_af_set(GPIOB, GPIO_AF_1, GPIO_PIN_4); /* configure PB5(TIMER2 CH1) as alternate function */ gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_5); gpio_output_options_set(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ,GPIO_PIN_5); gpio_af_set(GPIOB, GPIO_AF_1, GPIO_PIN_5); /* configure PB0(TIMER2 CH2) as alternate function */ gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_0); gpio_output_options_set(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ,GPIO_PIN_0); gpio_af_set(GPIOB, GPIO_AF_1, GPIO_PIN_0); /* configure PB1(TIMER2 CH3) as alternate function */ gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_1); gpio_output_options_set(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ,GPIO_PIN_1); gpio_af_set(GPIOB, GPIO_AF_1, GPIO_PIN_1); }复制代码/*! \brief enable clock of the TIMER peripheral \param[in] none \param[out] none \retval none */ void pwm_timer_clock_enable() { rcu_periph_clock_enable(RCU_TIMER2); }复制代码void pwm_timer_config(uint32_t timer) { /* ----------------------------------------------------------------------- TIMER2CLK is 100KHz TIMER2 channel0 duty cycle = (25000/ 50000)* 100 = 50% ----------------------------------------------------------------------- */ timer_oc_parameter_struct timer_ocintpara; timer_parameter_struct timer_initpara; timer_deinit(timer); /* TIMER configuration */ timer_initpara.prescaler = 719; timer_initpara.alignedmode = TIMER_COUNTER_EDGE; timer_initpara.counterdirection = TIMER_COUNTER_UP; timer_initpara.period = 1999; timer_initpara.clockdivision = TIMER_CKDIV_DIV1; timer_initpara.repetitioncounter = 0; timer_init(timer,&timer_initpara); /* configurate CH0 in PWM mode0 */ timer_ocintpara.ocpolarity = TIMER_OC_POLARITY_HIGH; timer_ocintpara.outputstate = TIMER_CCX_ENABLE; timer_channel_output_config(timer,TIMER_CH_0,&timer_ocintpara); timer_channel_output_pulse_value_config(timer,TIMER_CH_0,150-1); timer_channel_output_mode_config(timer,TIMER_CH_0,TIMER_OC_MODE_PWM0); timer_channel_output_shadow_config(timer,TIMER_CH_0,TIMER_OC_SHADOW_DISABLE); timer_auto_reload_shadow_enable(timer); timer_enable(timer); delay_1ms(500); timer_disable(timer); timer_channel_output_config(timer,TIMER_CH_1,&timer_ocintpara); timer_channel_output_pulse_value_config(timer,TIMER_CH_1,150-1); timer_channel_output_mode_config(timer,TIMER_CH_1,TIMER_OC_MODE_PWM0); timer_channel_output_shadow_config(timer,TIMER_CH_1,TIMER_OC_SHADOW_DISABLE); timer_auto_reload_shadow_enable(timer); timer_enable(timer); delay_1ms(500); timer_disable(timer); timer_channel_output_config(timer,TIMER_CH_2,&timer_ocintpara); timer_channel_output_pulse_value_config(timer,TIMER_CH_2,250-1); timer_channel_output_mode_config(timer,TIMER_CH_2,TIMER_OC_MODE_PWM0); timer_channel_output_shadow_config(timer,TIMER_CH_2,TIMER_OC_SHADOW_DISABLE); timer_auto_reload_shadow_enable(timer); timer_enable(timer); delay_1ms(500); timer_disable(timer); timer_channel_output_config(timer,TIMER_CH_3,&timer_ocintpara); timer_channel_output_pulse_value_config(timer,TIMER_CH_3,160-1); //160~200, Claw form close to open timer_channel_output_mode_config(timer,TIMER_CH_3,TIMER_OC_MODE_PWM0); timer_channel_output_shadow_config(timer,TIMER_CH_3,TIMER_OC_SHADOW_DISABLE); timer_auto_reload_shadow_enable(timer); timer_enable(timer); delay_1ms(500); }复制代码 然后,如果要改变特定通道的PWM宽度,可以用timer_channel_output_pulse_value_config(TIMER2,TIMER_CH_0, value)函数. 由于舵机瞬态电流比较大,务必增大电流或者制作专用驱动电路,否则GD32E231会因为电压瞬间降低而引起复位。 我这里采用5V 3A的供电加上USB供电,并且在程序中,平滑PWM值的改变,才勉强实现系统的稳定工作。 可以采用这种法法来实现舵机平滑切换: /*! \brief config the specific steering motor 0,1,2,3 \param[in] motor_tag: 0,1,2,3; pulse: 50~250, mid=150 0.5ms~2.5ms \param[out] none \retval none */ void control_motor(uint8_t motor_tag, uint32_t pulse) { uint8_t i; if(motor_tag == 0) { if(pulse>=current_value[0]) { for(i=current_value[0];i = pulse; i--) //in case of big current to pulse MCU system { timer_channel_output_pulse_value_config(TIMER2,TIMER_CH_0,i-1); delay_1ms(20); } } current_value[0] = pulse; } else if (motor_tag == 1) { if(pulse>=current_value[1]) { for(i=current_value[1];i = pulse; i--) //in case of big current to pulse MCU system { timer_channel_output_pulse_value_config(TIMER2,TIMER_CH_1,i-1); delay_1ms(20); } } current_value[1] = pulse; } else if (motor_tag == 2) { if(pulse>=current_value[2]) { for(i=current_value[2];i = pulse; i--) //in case of big current to pulse MCU system { timer_channel_output_pulse_value_config(TIMER2,TIMER_CH_2,i-1); delay_1ms(30); } } current_value[2] = pulse; } else if (motor_tag == 3) { if(pulse>=current_value[3]) { for(i=current_value[3];i = pulse; i--) //in case of big current to pulse MCU system { timer_channel_output_pulse_value_config(TIMER2,TIMER_CH_3,i-1); delay_1ms(50); } } current_value[3] = pulse; } }复制代码 主函数中: pwm_timer_clock_enable(); pwm_timer_config(TIMER2);复制代码

  • 回复了主题帖: “我和intel SoC FPGA”+ 搞不懂的arm硬核开发

    小梅哥 发表于 2019-5-19 14:38 如果只是简单的物联网,不需要高速数据交互,其实还是蛮简单的。基本上把Linux应用开发掌握熟练就可以了。 ...
    嗯嗯,一直就是零散的学,我这边抽空把linux和一些网络基础补补,多谢小梅哥指点

  • 2019-05-14
  • 回复了主题帖: [颁奖]读RT-Thread技术好书,写读书笔记

    信息正确,多谢论坛~

  • 2019-05-12
  • 发表了主题帖: 【RT-Thread读书笔记】RT-Thread 读后感整体总结

    本帖最后由 传媒学子 于 2019-5-12 16:54 编辑 【RT-Thread读书笔记】RT-Thread 读后感整体总结 今天是活动的最后一天,一个月的时间悄然而过。非常感谢论坛举办的这个读书月活动,让我可以静下心来深入学习一款RTOS。 从拿到这本书开始,到最后了解RT-Thread, 确实花费了一些时间。 RT-Thread作为国产RTOS, 能够发展如此壮大,着实让我兴奋不已。 我在官方论坛上也学习到了不少知识,不仅仅是RT-Thread的知识,github上公开的源码,可以帮助大家学习code的风格,学习现代化工程代码管理的组织形式,以及了解git的代码管理。 一个月虽短,但让我领略了一种RTOS, 我深知这只是开始,在后续的日子里,慢慢体会,争取能够将这款RTOS的精髓掌握,成为一名优秀的嵌入式爱好者。 此内容由EEWORLD论坛网友传媒学子原创,如需转载或用于商业用途需征得作者同意并注明出处

  • 发表了主题帖: 【RT-Thread读书笔记】13. RT-Thread 学习24-26章读后感

    本帖最后由 传媒学子 于 2019-5-12 16:10 编辑 24章 内存管理 计算系统中,通常存储空间可以分为两种:内部存储空间和外部存储空间。内部存储空间通常访问速度比较快,能够按照变量地址随机地访问,也就是我们通常所说的 RAM(随机存储器),可以把它理解为电脑的内存;而外部存储空间内所保存的内容相对来说比较固定,即使掉电后数据也不会丢失,这就是通常所讲的 ROM(只读存储器),可以把它理解为电脑的硬盘。计算机系统中,变量、中间数据一般存放在 RAM 中,只有在实际使用时才将它们从 RAM 调入到 CPU 中进行运算。一些数据需要的内存大小需要在程序运行过程中根据实际情况确定,这就要求系统具有对内存空间进行动态管理的能力,在用户需要一段内存空间时,向系统申请,系统选择一段合适的内存空间分配给用户,用户使用完毕后,再释放回系统,以便系统将该段内存空间回收再利用。这章主要介绍 RT-Thread 中的两种内存管理方式,分别是动态内存堆管理和静态内存池管理,学完本章,读者会了解 RT-Thread 的内存管理原理及使用方式。RT-Thread 操作系统在内存管理上,根据上层应用及系统资源的不同,有针对性地提供了不同的内存分配管理算法。总体上可分为两类:内存堆管理与内存池管理,而内存堆管理又根据具体内存设备划分为三种情况:第一种是针对小内存块的分配管理(小内存管理算法);第二种是针对大内存块的分配管理(slab 管理算法);第三种是针对多内存堆的分配情况(memheap 管理算法)----引自 https://www.rt-thread.org/document/site/programming-manual/memory/memory/ 其实,内存管理是OS中一个必备的组件,如何没有内存管理,OS如何运行,我们常见的linux和windos都是运行在内存中,这个内存指的是RAM, 内存条DRAM。有些数据还存在CPU内部的高速缓存中,L1/L2/L3 cache中。而这些内存的速度是非常快的,但是掉电不保存。而真正的程序,数据是存放在硬盘中的,速度慢,但掉电保存。当OS运行后,每个要运行的程序,都会先被读取到内存中,然后才开始运行,最终数据会被写入硬盘中保存。 这里面牵扯到内存一致性等问题,x86体系中有MESI关于CPU核内,cache一致性的描述,而RTOS由于一般只有一个core, 也许没有MESI机制。但内存管理机制,就不可缺少的。 这里啰嗦了一些知识,下面回到书中的内存管理。 基本概念 RT-Thread操作系统将内核与内存管理分开实现,操作系统内核仅规定了必要的内存管理函数原型,而不关心这些内存管理函数是如何实现的,所以在RT-Thread中提供了多种内存分配算法(分配策略),但是上层接口(API)却是统一的。RT-Thread的内存管理模块管理系统的内存资源,它是操作系统的核心模块之一。主要包括内存的初始化、分配以及释放。 运作机制 首先,在使用内存分配前,必须明白自己在做什么,这样做与其它的方法有什么不同,特别是会产生哪些负面影响,在自己的产品面前,应当选择哪种分配策略。 动态分配内存与静态分配内存的区别:静态内存一旦创建就指定了内存块的大小,分配只能以内存块大小粒度进行分配;动态内存分配则根据运行时环境确定需要的内存块大小,按照需要分配内存。 静态内存管理: 内存池(Memory Pool)是一种用于分配大量大小相同的小内存对象的技术。它可以极大加快内存分配/释放的速度。 内核负责给内存池分配内存池对象控制块,它同时也接收用户线程的分配内存块申请,当获得申请信息后,内核就可以从内存池中为线程分配内存块。内存池一旦初始化完成,内部的内存块大小将不能再做调整。 动态内存管理: 动态内存管理是一个真实的堆(Heap)内存管理模块。动态内存管理,即在内存资源充足的情况下,从系统配置的一块比较大的连续内存,根据用户需求,在这块内存中分配任意大小的内存块。当用户不需要该内存块时,又可以释放回系统供下一次使用。与静态内存相比,动态内存管理的好处是按需分配,缺点是内存池中容易出现碎片(在申请与释放的时候由于内存不对齐会导致内存碎片)。 1.小内存管理模块 小内存管理算法是一个简单的内存分配算法。初始时,它是一块大的内存,其大小为(MEM_SIZE),当需要分配内存块时,将从这个大的内存块上分割出相匹配的内存块,然后把分割出来的空闲内存块还回给堆管理系统中。每个内存块都包含一个管理用的数据头,通过这个头把使用块与空闲块用双向链表的方式链接起来(内存块链表)。 2.SLAB内存管理模块 RT-Thread的SLAB分配器是在DragonFly BSD创始人Matthew Dillon实现的SLAB分配器基础上,针对嵌入式系统优化的内存分配算法。最原始的SLAB算法是Jeff Bonwick为Solaris操作系统而引入的一种高效内核内存分配算法。 RT-Thread的SLAB分配器实现主要是去掉了其中的对象构造及析构过程,只保留了纯粹的缓冲型的内存池算法。SLAB分配器会根据对象的类型(主要是大小)分成多个区(zone),也可以看成每类对象有一个内存池。 静态内存管理的函数接口: rt_mp_create(), rt_mp_alloc(),rt_mp_free(). 动态内存管理的函数接口: rt_system_heap_init(),rt_malloc(),rt_free(). 25章 中断管理 异常是导致处理器脱离正常运行转向执行特殊代码的任何事件,分为同步异常和异步异常,中断属于异步异常。中断能打断线程的运行,无论该线程具有什么样的优先级,因此中断一般用于处理比较紧急的事件,而且只做简单处理,例如标记该事件,在使用 RT-Thread系统时,一般建议使用信号量、消息或事件标志组等标志中断的发生,将这些内核对象发布给处理线程,处理线程再做具体处理。 通过中断机制,在外设不需要CPU介入时,CPU可以执行其它线程,而当外设需要CPU时通过产生中断信号使CPU立即停止当前线程转而来响应中断请求。这样可以使CPU避免把大量时间耗费在等待、查询外设状态的操作上,因此将大大提高系统实时性以及执行效率。 因此,前边所讲临界段,不能被中断打断,这样会影响RTOS的实时性。任何使用了操作系统的中断响应都不会比裸机快。所以,操作系统的中断在某些时候会有适当的中断延迟,因此调用中断屏蔽函数进入临界段的时候,也需快进快出。 中断介绍: 与中断相关的硬件可以划分为三类:外设、中断控制器、CPU本身。 CPU:CPU会响应中断源的请求,中断当前正在执行的线程,转而执行中断处理程序。NVIC最多支持240个中断,每个中断最多256个优先级。 中断号:每个中断请求信号都会有特定的标志,使得计算机能够判断是哪个设备提出的中断请求,这个标志就是中断号。 中断请求:“紧急事件”需向CPU提出申请,要求CPU暂停当前执行的线程,转而处理该“紧急事件”,这一申请过程称为中断请求。 中断优先级:为使系统能够及时响应并处理所有中断,系统根据中断时间的重要性和紧迫程度,将中断源分为若干个级别,称作中断优先级。 中断处理程序:当外设产生中断请求后,CPU暂停当前的线程,转而响应中断申请,即执行中断处理程序。 中断触发:中断源发出并送给CPU控制信号,将中断触发器置“1”,表明该中断源产生了中断,要求CPU去响应该中断,CPU暂停当前线程,执行相应的中断处理程序。 中断触发类型:外部中断申请通过一个物理信号发送到NVIC,可以是电平触发或边沿触发。 中断向量:中断服务程序的入口地址。 中断向量表:存储中断向量的存储区,中断向量与中断号对应,中断向量在中断向量表中按照中断号顺序存储。 临界段:代码的临界段也称为临界区,一旦这部分代码开始执行,则不允许任何中断打断。为确保临界段代码的执行不被中断,在进入临界段之前须关中断,而临界段代码执行完毕后,要立即开中断。RT-Thread支持中断屏蔽和中断使能。 当中断产生时,处理机将按如下的顺序执行: 1. 保存当前处理机状态信息 2. 载入异常或中断处理函数到PC寄存器 3. 把控制权转交给处理函数并开始执行 4. 当处理函数执行完成时,恢复处理器状态信息 5. 从异常或中断中返回到前一个程序执行点 中断管理: ARM Cortex-M内核的中断是不受RT-Thread管理的,所以RT-Thread中的中断使用其实跟裸机差不多的,需要我们自己配置中断,并且使能中断,编写中断服务函数,在中断服务函数中使用内核IPC通信机制,一般建议使用信号量、消息或事件标志组等标志事件的发生,将事件发布给处理线程,等退出中断后再由相关处理线程具体处理中断。由于中断不受RT-Thread管理,所以不需要使用RT-Thread提供的函数(中断屏蔽与使能除外)。 第26章 双向链表 这一章讲的是双向链表,双向链表对于实现RT-Thread非常重要,创建线程,线程优先级,线程就绪列表等地方都用到了双向链表,建议大家在学习数据结构时,必须深刻掌握这一数据结构。 这一章都是基本的数据结构,所以不再展开介绍,可能火哥觉得这一部分非常重要,因此放在本书的最后。 此内容由EEWORLD论坛网友传媒学子原创,如需转载或用于商业用途需征得作者同意并注明出处

  • 发表了主题帖: “我和intel SoC FPGA”+ 搞不懂的arm硬核开发

    本帖最后由 传媒学子 于 2019-5-12 14:46 编辑 “我和intel SoC FPGA”+ 搞不懂的arm硬核开发 为何选择intel SoC FPGA? 记得上大学时,开始学FPGA,那时没有什么钱,网上一大堆Cyclone II的开发板,比较便宜,100元起,而且资料很齐全,也就从那时起,开始认识Altera FPGA, 也就是现在的intel FPGA,不过一直都是写写verilog, 没有涉及到SOC,硬核了什么的,连软核nios也是一知半解。硕士毕业前夕,感觉自己应该搞点牛逼的东西,便自己掏了800大洋,从友晶科技那里买来了最新的DE10-nano开发板,貌似是带两个arm硬核,鼓捣了几天,用到了linux什么的,后来觉得太复杂,也没什么项目支撑,就放在那里落灰了。 遇到的难点 我会nios,也会verilog,也会arm MCU的编程,但是在FPGA内部嵌入的硬核,我始终不是太明白,它到低和FPGA是怎么联系起来的,内部是什么总线?内部FLASH, RAM是怎么分配的,如何启动arm, 配置arm,这些都不懂。 而且资料都是英文,也比较难找,容易疲劳。 quartus prime用起来,也是比较熟悉的,比起以前quartusII, 更好用了。但是,不知道arm硬核用什么开发软件,貌似DS-5, 唉,很希望有一本好的入门教程,带我领略一下这些知识。 实际项目 我本来想用DE10-nano来搭建一个远程物联网平台,可是arm硬核没有搞懂,就比较郁闷,由于也找不到什么简单的引导资料,因此模块都买好了,但项目却一直停在那里。 希望能够,得到小梅哥的这本《SoC FPGA 嵌入式设计和开发教程》,让落灰的板子继续run起来。

  • 回复了主题帖: Altera DE1-SOC培训教材

    好资料

  • 2019-05-11
  • 发表了主题帖: 【RT-Thread读书笔记】12. RT-Thread 学习21-23章读后感

    本帖最后由 传媒学子 于 2019-5-11 23:04 编辑 第21章 事件 概念 事件集合用32位无符号整型变量来表示,每一位代表一个事件,线程通过“逻辑与”或“逻辑或”与一个或多个事件建立关联,形成一个事件集。事件的“逻辑或”也称作是独立型同步,指的是线程感兴趣的所有事件任一件发生即可被唤醒;事件“逻辑与”也称为是关联型同步,指的是线程感兴趣的若干事件都发生时才被唤醒。 这个很好理解,需要注意的是RT-Thread的事件仅用于同步,不提供数据传输功能。在RT-Thread实现中,每个线程都拥有一个事件信息标记,它有三个属性,分别是RT_EVENT_FLAG_AND(逻辑与),RT_EVENT_FLAG_OR(逻辑或)以及RT_EVENT_FLAG_CLEAR(清除标记)。当线程等待事件同步时,可以通过32个事件标志和这个事件信息标记来判断当前接收的事件是否满足同步条件。 应用场景 事件可使用于多种场合,它能够在一定程度上替代信号量,用于线程间同步。 事件控制块 /* * event structure */ struct rt_event { struct rt_ipc_object parent; /**< inherit from ipc_object */ rt_uint32_t set; /**< event set */ }; typedef struct rt_event *rt_event_t;复制代码 事件属于内核对象,也会在自身结构体里面包含一个内核对象类型的成员,通过这个成员可以将事件挂到系统对象容器里面。rt_event对象从rt_ipc_object中派生,由IPC容器管理。 事件函数接口有:rt_event_create(), rt_event_delete(), rt_event_send(), rt_event_recv(). 第22章 软件定时器 基本概念 定时器有硬件定时器和软件定时器之分: 硬件定时器是芯片本身提供的定时功能。一般是由外部晶振提供给芯片输入时钟,芯片向软件模块提供一组配置寄存器,接受控制输入,到达设定时间值后芯片中断控制器产生时钟中断。硬件定时器的精度一般很高,可以达到纳秒级别,并且是中断触发方式。 软件定时器,软件定时器是由操作系统提供的一类系统接口,它构建在硬件定时器基础之上,使系统能够提供不受硬件定时器资源限制的定时器服务,它实现的功能与硬件定时器也是类似的。 RT-Thread操作系统提供软件定时器功能,软件定时器的使用相当于扩展了定时器的数量,允许创建更多的定时业务。 RT-Thread提供的软件定时器支持单次模式和周期模式,单次模式和周期模式的定时时间到之后都会调用定时器的超时函数,用户可以在超时函数中加入要执行的工程代码。 单次模式:当用户创建了定时器并启动了定时器后,定时时间到了,只执行一次超时函数之后就将该定时器删除,不再重新执行。 周期模式:这个定时器会按照设置的定时时间循环执行超时函数,直到用户将定时器删除。 RT-Thread中在rtdef.h中定义了相关的宏定义来选择定时器的工作模式:  RT_TIMER_FLAG_HARD_TIMER 为硬件定时器。  RT_TIMER_FLAG_SOFT_TIMER为软件定时器。 应用场景 硬件定时器数量有限,因此有时需要采用软件定时器,软件定时器的精度不高。但需要注意的是软件定时器的精度是无法和硬件定时器相比的,因为在软件定时器的定时过程中是极有可能被其它的线程所打断,因为软件定时器的线程优先级是RT_TIMER_THREAD_PRIO,默认为4。所以,软件定时器更适用于对时间精度要求不高的线程,一些辅助型的线程。 运行机制 软件定时器是系统资源,在创建定时器的时候会分配一块内存空间。当用户创建并启动一个软件定时器时, RT-Thread会根据当前系统rt_tick时间及用户设置的定时确定该定时器唤醒时间timeout,并将该定时器控制块挂入软件定时器列表rt_soft_timer_list。 在RT-Thread定时器模块中维护着两个重要的全局变量: 1. rt_tick,它是一个32位无符号的变量,用于记录当前系统经过的tick时间,当硬件定时器中断来临时,它将自动增加1。 2.软件定时器列表rt_soft_timer_list。系统新创建并激活的定时器都会以超时时间升序的方式插入到rt_soft_timer_list列表中。系统在定时器线程中扫描rt_soft_timer_list中的第一个定时器,看是否已超时,若已经超时了则调用软件定时器超时函数。 否则出软件定时器线程,因为定时时间是升序插入软件定时器列表的,列表中第一个定时器的定时时间都还没到的话,那后面的定时器定时时间自然没到。 软件定时器的相关函数 rt_timer_create(); rt_timer_t rt_timer_create(const char *name, void (*timeout)(void *parameter), void *parameter, rt_tick_t time, rt_uint8_t flag) { struct rt_timer *timer; /* allocate a object */ timer = (struct rt_timer *)rt_object_allocate(RT_Object_Class_Timer, name); if (timer == RT_NULL) { return RT_NULL; } _rt_timer_init(timer, timeout, parameter, time, flag); return timer; }复制代码 第23章 邮箱 概念 IPC(Inter-Process Communication,进程间通信)邮箱相比于信号量与消息队列来说,其开销更低,效率更高,所以常用来做线程与线程、中断与线程间的通信。 邮箱中的每一封邮件只能容纳固定的4字节内容(STM32是32位处理系统,一个指针的大小即为4个字节,所以一封邮件恰好能够容纳一个指针),当需要在线程间传递比较大的消息时,可以把指向一个缓冲区的指针作为邮件发送到邮箱中。 通过邮箱,线程或中断服务函数可以将一个或多个邮件放入邮箱中。同样,一个或多个线程可以从邮箱中获得邮件消息。当有多个邮件发送到邮箱时,通常应将先进入邮箱的邮件先传给线程,也就是说,线程先得到的是最先进入邮箱的消息,即先进先出原则(FIFO),同时RT-Thread中的邮箱支持优先级,也就是说在所有等待邮件的线程中优先级最高的会先获得邮件。 邮箱中的每一封邮件只能容纳固定的4字节内容(可以存放地址)。 运作机制 创建邮箱对象时会先创建一个邮箱对象控制块,然后给邮箱分配一块内存空间用来存放邮件,这块内存的大小等于邮件大小(4字节)与邮箱容量的乘积,接着初始化接收邮件和发送邮件在邮箱中的偏移量,接着再初始化消息队列,此时消息队列为空。 RT-Thread操作系统的邮箱对象由多个元素组成,当邮箱被创建时,它就被分配了邮箱控制块:邮箱名称、邮箱缓冲区起始地址、邮箱大小等。同时每个邮箱对象中包含着多个邮件框,每个邮件框可以存放一封邮件;所有邮箱中的邮件框总数即是邮箱的大小,这个大小可在邮箱创建时指定。 非阻塞方式的邮件可以在中断和线程间相互发送,但阻塞方式的邮件只能在线程间发送。 邮箱运行机制: 邮箱控制块: /** * mailbox structure */ struct rt_mailbox { struct rt_ipc_object parent; /**< inherit from ipc_object */ rt_uint32_t *msg_pool; /**< start address of message buffer */ rt_uint16_t size; /**< size of message pool */ rt_uint16_t entry; /**< index of messages in msg_pool */ rt_uint16_t in_offset; /**< input offset of the message buffer */ rt_uint16_t out_offset; /**< output offset of the message buffer */ rt_list_t suspend_sender_thread; /**< sender thread suspended on this mailbox */ }; typedef struct rt_mailbox *rt_mailbox_t;复制代码 相关函数 rt_mb_create(),rt_mb_delete(), rt_mb_send_wait()(阻塞),rt_mb_send ()(非阻塞),rt_mb_recv(). 此内容由EEWORLD论坛网友传媒学子原创,如需转载或用于商业用途需征得作者同意并注明出处

  • 发表了主题帖: 【RT-Thread读书笔记】11. RT-Thread 学习18-20章读后感

    本帖最后由 传媒学子 于 2019-5-11 22:29 编辑 第18章 消息队列 在学习这章之前,建议先复习一下队列的知识。 队列又称消息队列,是一种常用于线程间通信的数据结构,队列可以在线程与线程间、中断和线程间传送信息,实现了线程接收来自其他线程或中断的不固定长度的消息,并根据不同的接口选择传递消息是否存放在线程自己的空间。 通过消息队列服务,线程或中断服务例程可以将一条或多条消息放入消息队列中。同样,一个或多个线程可以从消息队列中获得消息。当有多个消息发送到消息队列时,通常是将先进入消息队列的消息先传给线程,也就是说,线程先得到的是最先进入消息队列的消息,即先进先出原则(FIFO)。同时RT-Thread中的消息队列支持优先级,也就是说在所有等待消息的线程中优先级最高的会先获得消息。 消息队列的运作机制: 创建消息队列时先创建一个消息队列对象控制块,然后给消息队列分配一块内存空间,组织成空闲消息链表,这块内存的大小等于[消息大小+消息头(用于链表连接)]与消息队列容量的乘积,接着再初始化消息队列,此时消息队列为空。 RT-Thread操作系统的消息队列对象由多个元素组成,当消息队列被创建时,它就被分配了消息队列控制块:消息队列名称、内存缓冲区、消息大小以及队列长度等。 发送消息并不带有阻塞机制;当空闲消息链表上无可用消息块,说明消息队列已满,此时,发送消息的的线程或者中断程序会收到一个错误码(-RT_EFULL)。 接收消息有阻塞机制,线程在没有获取到自己的消息时,可以一直等待,直到获取相关消息。 运作过程: 消息队列控制块: /** * message queue structure */ struct rt_messagequeue {     struct rt_ipc_object parent;                        /**< inherit from ipc_object */     void                *msg_pool;                      /**< start address of message queue */     rt_uint16_t          msg_size;                      /**< message size of each message */     rt_uint16_t          max_msgs;                      /**< max number of messages */     rt_uint16_t          entry;                         /**< index of messages in the queue */     void                *msg_queue_head;                /**< list head */     void                *msg_queue_tail;                /**< list tail */     void                *msg_queue_free;                /**< pointer indicated the free node of queue */ }; typedef struct rt_messagequeue *rt_mq_t;复制代码 消息队列可以应用于发送不定长消息的场合,包括线程与线程间的消息交换,以及在中断服务函数中给线程发送消息(中断服务例程不可能接收消息)。 常用消息队列的函数:  创建消息队列rt_mq_create。  写队列操作函数rt_mq_send。  读队列操作函数rt_mq_recv。  删除队列rt_mq_delete。 第19章 信号量 信号量(Semaphore)是一种实现线程间通信的机制,实现线程之间同步或临界资源的互斥访问,常用于协助一组相互竞争的线程来访问临界资源。 在操作系统中,我们使用信号量的目的是为了给临界资源建立一个标志,信号量表示了该临界资源被占用情况。这样,当一个线程在访问临界资源的时候,就会先对这个资源信息进行查询,从而在了解资源被占用的情况之后,再做处理,从而使得临界资源得到有效的保护。 信号量还有计数型信号量,计数型信号量允许多个线程对其进行操作,但限制了线程的数量。 在嵌入式操作系统中二值信号量是线程间、线程与中断间同步的重要手段。 用1和0来代表该信号量是否可以被可用。因为信号量资源被获取了,信号量值就是 0,信号量资源被释放,信号量值就是 1。 例如:某个线程需要等待一个标记,那么线程可以在轮询中查询这个标记有没有被置位,这样子做,就会很消耗CPU资源,其实根本不需要在轮询中查询这个标记,只需要使用二值信号量即可,当二值信号量没有的时候,线程进入阻塞态等待二值信号量到来即可,当得到了这个信号量(标记)之后,在进行线程的处理即可,这样子么就不会消耗太多资源了,而且实时响应也是最快的。 二值信号量在线程与线程中同步的应用场景:假设我们有一个温湿度的传感器,假设是1s采集一次数据,那么我们让他在液晶屏中显示数据出来,这个周期也是要1s一次的,如果液晶屏刷新的周期是100ms更新一次,那么此时的温湿度的数据还没更新,液晶屏根本无需刷新,只需要在1s后温湿度数据更新的时候刷新即可,否则CPU就是白白做了多次的无效数据更新,CPU的资源就被刷新数据这个线程占用了大半,造成CPU资源浪费,如果液晶屏刷新的周期是10s更新一次,那么温湿度的数据都变化了10次,液晶屏才来更新数据,那拿这个产品有啥用,根本就是不准确的,所以,还是需要同步协调工作,在温湿度采集完毕之后,进行液晶屏数据的刷新,这样子,才是最准确的,并且不会浪费CPU的资源。 同理,二值信号量在线程与中断同步的应用场景:我们在串口接收中,我们不知道啥时候有数据发送过来,有一个线程是做接收这些数据处理,总不能在线程中每时每刻都在线程查询有没有数据到来,那样会浪费CPU资源,所以在这种情况下使用二值信号量是很好的办法,当没有数据到来的时候,线程就进入阻塞态,不参与线程的调度,等到数据到来了,释放一个二值信号量,线程就立即从阻塞态中解除,进入就绪态,然后运行的时候处理数据,这样子系统的资源就会很好的被利用起来。 二值信号量的运作机制 创建二值信号量,为创建的信号量对象分配内存,并把可用信号量初始化为用户自定义的个数, 二值信号量的最大可用信号量个数为1。 信号量获取,从创建的信号量资源中获取一个信号量,获取成功返回正确。否则线程会等待其它线程释放该信号量,超时时间由用户设定。当线程获取信号量失败时,线程将进入阻塞态,系统将线程挂到该信号量的阻塞列表中。假如某个时间中断/线程释放了信号量,那么,由于获取无效信号量而进入阻塞态的线程将获得信号量并且恢复为就绪态。 计数型信号量的运作机制 计数型信号量与二值信号量其实都是差不多的,一样用于资源保护,不过计数信号量则允许多个线程获取信号量访问共享资源,但会限制线程的最大数目。访问的线程数达到信号量可支持的最大数目时,会阻塞其他试图获取该信号量的线程,直到有线程释放了信号量。 信号量控制块: /** * Semaphore structure */ struct rt_semaphore {     struct rt_ipc_object parent;                        /**< inherit from ipc_object */     rt_uint16_t          value;                         /**< value of semaphore. */ }; typedef struct rt_semaphore *rt_sem_t;复制代码 常用的信号量函数有:rt_sem_create(), rt_sem_delete(), rt_sem_release(), rt_sem_take(), 第20章 互斥量 为了避免递归获取信号量时发生主动挂起引起死锁,在二值信号量中又规定了一种特殊的信号量,成为互斥信号量。它和信号量不同的是,它支持互斥量所有权、递归访问以及防止优先级翻转的特性,用于实现对临界资源的独占式处理。任意时刻互斥量的状态只有两种,开锁或闭锁。当互斥量被线程持有时,该互斥量处于闭锁状态,这个线程获得互斥量的所有权。当该线程释放这个互斥量时,该互斥量处于开锁状态,线程失去该互斥量的所有权。当一个线程持有互斥量时,其他线程将不能再对该互斥量进行开锁或持有。 优先级翻转是当一个高优先级任务通过信号量机制访问共享资源时,该信号量已被一低优先级任务占有,因此造成高优先级任务被许多具有较低优先级任务阻塞,实时性难以得到保证。低优先级占有信号量,但又无法及时得到执行,造成高优先级任务阻塞,因此必须想办法让低优先级任务执行。 在RT-Thread操作系统中为了降低优先级翻转问题利用了优先级继承算法。优先级继承算法是指,暂时提高某个占有某种资源的低优先级线程的优先级,使之与在所有等待该资源的线程中优先级最高那个线程的优先级相等,而当这个低优先级线程执行完毕释放该资源时,优先级重新回到初始设定值。因此,继承优先级的线程避免了系统资源被任何中间优先级的线程抢占。 互斥量与二值信号量最大的不同是:互斥量具有优先级继承机制,而信号量没有。也就是说,某个临界资源受到一个互斥量保护,如果这个资源正在被一个低优先级线程使用,那么此时的互斥量是闭锁状态,也代表了没有线程能申请到这个互斥量,如果此时一个高优先级线程想要对这个资源进行访问,去申请这个互斥量,那么高优先级线程会因为申请不到互斥量而进入阻塞态,那么系统会将现在持有该互斥量的线程的优先级临时提升到与高优先级线程的优先级相同,这个优先级提升的过程叫做优先级继承。这个优先级继承机制确保高优先级线程进入阻塞状态的时间尽可能短,以及将已经出现的“优先级翻转”危害降低到最小。 需要注意的是互斥量不能在中断服务函数中使用。 互斥量控制块: /** * Mutual exclusion (mutex) structure */ struct rt_mutex {     struct rt_ipc_object parent;                        /**< inherit from ipc_object */     rt_uint16_t          value;                         /**< value of mutex */     rt_uint8_t           original_priority;             /**< priority of last thread hold the mutex */     rt_uint8_t           hold;                          /**< numbers of thread hold the mutex */     struct rt_thread    *owner;                         /**< current owner of mutex */ }; typedef struct rt_mutex *rt_mutex_t;复制代码 相关接口函数: rt_mutex_create(), rt_mutex_delete(), rt_mutex_release(), rt_mutex_take(). 使用互斥量时候需要注意几点: 1. 两个线程不能对同时持有同一个互斥量。如果某线程对已被持有的互斥量进行获取,则该线程会被挂起,直到持有该互斥量的线程将互斥量释放成功,其他线程才能申请这个互斥量。 2. 互斥量不能在中断服务程序中使用。 3. RT-Thread作为实时操作系统需要保证线程调度的实时性,尽量避免线程的长时间阻塞,因此在获得互斥量之后,应该尽快释放互斥量。 4. 持有互斥量的过程中,不得再调用rt_thread_control()等函数接口更改持有互斥量线程的优先级。 此内容由EEWORLD论坛网友传媒学子原创,如需转载或用于商业用途需征得作者同意并注明出处

  • 发表了主题帖: 【RT-Thread读书笔记】10. RT-Thread 学习17章读后感

    第17章 线程管理17.1 线程的基本概念 线程是RTOS的精髓,小到嵌入式OS大到桌面电脑服务器的OS,linux/windows等都有线程概念,线程是处理任务的基本单元。常常听Intel的超线程,1个CPU内可以运行2个线程,只有i7支持,逻辑核多了1个。 其实,线程的概念是软件层面的,硬件层面就是执行code, 执行指令。 好了闲话不多说,直接回答书上。 这一章主要涉及线程管理,因此首先回顾了一下线程的基本概念,这里要说的是栈对线程切换的意义,所有线程都需要堆栈来保存其上下文。堆是一个完全二叉树,堆的性质决定了堆在RTOS是一个重要的数据结构,大家可以再温习一下数据结构。 另外:RT-Thread中的线程是抢占式调度机制,同时支持时间片轮转调度方式。高优先级的线程可打断低优先级线程,低优先级线程必须在高优先级线程阻塞或结束后才能得到调度。 17.2 线程调度器的基本概念 RT-Thread中提供的线程调度器是基于优先级的全抢占式调度:在系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的,包括线程调度器自身。 RT-Thread内核中采用了基于位图的优先级算法(时间复杂度O(1),即与就绪线程的多少无关),通过位图的定位快速的获得优先级最高的线程,具体见10.1.1 小节。 这一点不得不说,确实非常巧妙,否则,查找优先级最高的线程就会耗费大量的时间,造成OS实时性降低。 RT-Thread内核中也允许创建相同优先级的线程。相同优先级的线程采用时间片轮转方式进行调度(也就是通常说的分时调度器),时间片轮转调度仅在当前系统中无更高优先级就绪线程存在的情况下才有效。 RT-Thread系统中的每一线程都有多种运行状态。系统初始化完成后,创建的线程就可以在系统中竞争一定的资源,由内核进行调度。 线程状态通常分为以下四种:初始态(RT_THREAD_INIT)、就绪态(RT_THREAD_READY)、运行态(RT_THREAD_RUNNING)、挂起态(RT_THREAD_SUSPEND)、关闭态(RT_THREAD_CLOSE)。 17.6 线程的设计要点 在设计之初就应该考虑下面几点因素:线程运行的上下文环境、线程的执行时间合理设计。 中断服务函数是一种需要特别注意的上下文环境,它运行在非线程的执行环境下(一般为芯片的一种特殊运行模式(也被称作特权模式)),在这个上下文环境中不能使用挂起当前线程的操作,不允许调用任何会阻塞运行的API函数接口。另外需要注意的是,中断服务程序最好保持精简短小,快进快出,一般在中断服务函数中只做标记事件的发生,让对应线程去执行相关处理,因为中断服务函数的优先级高于任何优先级的线程,如果中断处理时间过长,将会导致整个系统的线程无法正常运行。所以在设计的时候必须考虑中断的频率、中断的处理时间等重要因素,以便配合对应中断处理线程的工作。 线程的程序不能出现了死循环操作(此处的死循环是指没有不带阻塞机制的线程循环体),那么比这个线程优先级低的线程都将无法执行,当然也包括了空闲线程,因为死循环的时候,线程不会主动让出CPU,低优先级的线程是不可能得到CPU的使用权的,而高优先级的线程就可以抢占CPU。 空闲线程(idle线程)是RT-Thread系统中没有其他工作进行时自动进入的系统线程。用户可以通过空闲线程钩子方式,在空闲线程上钩入自己的功能函数。通常这个空闲线程钩子能够完成一些额外的特殊功能,例如系统运行状态的指示,系统省电模式等。除了空闲线程钩子,RT-Thread系统还把空闲线程用于一些其他的功能,比如当系统删除一个线程或一个动态线程运行结束时,会先行更改线程状态为非调度状态,然后挂入一个待回收队列中,真正的系统资源回收工作在空闲线程完成,空闲线程是唯一不允许出现阻塞情况的线程,因为RT-Thread需要保证系统用于都有一个可运行的线程。 此内容由EEWORLD论坛网友传媒学子原创,如需转载或用于商业用途需征得作者同意并注明出处

  • 发表了主题帖: 【RT-Thread读书笔记】9. RT-Thread 学习14-16章读后感

    14.创建线程首先,应当硬件初始化,然后再创建线程,硬件初始化是将线程中用到的硬件模块进行初始化。因为线程相当于裸机时的while循环前的初始化,例如使用ADC,先应当配置好ADC各项参数,通道等。 其次,需要就是创建线程。 一般包括:定义线程函数,定义线程栈,定义线程控制块,初始化线程,启动线程等。 本章讲解了在SARM静态内存和动态内存创建单线程和多线程task。 15.重映射串口到kt_printf函数 一般我们需要使用串口来来打印一些信息,辅助我们调试。 RT-Thread中有rt_kprintf()接口函数供我们使用,我们可以把串口、CAN、USB、以太网等输出设备映射过来,一般情况下,我们会采用串口。 rt_kprintf()函数在kservice.c中实现,是属于内核服务类的函数: /** * This function will print a formatted string on system console * * @param fmt the format */ void rt_kprintf(const char *fmt, ...) {     va_list args;     rt_size_t length;     static char rt_log_buf[RT_CONSOLEBUF_SIZE];     va_start(args, fmt);     /* the return value of vsnprintf is the number of bytes that would be      * written to buffer had if the size of the buffer been sufficiently      * large excluding the terminating null byte. If the output string      * would be larger than the rt_log_buf, we have to adjust the output      * length. */     length = rt_vsnprintf(rt_log_buf, sizeof(rt_log_buf) - 1, fmt, args);     if (length > RT_CONSOLEBUF_SIZE - 1)         length = RT_CONSOLEBUF_SIZE - 1; #ifdef RT_USING_DEVICE     if (_console_device == RT_NULL)     {         rt_hw_console_output(rt_log_buf);     }     else     {         rt_uint16_t old_flag = _console_device->open_flag;         _console_device->open_flag |= RT_DEVICE_FLAG_STREAM;         rt_device_write(_console_device, 0, rt_log_buf, length);         _console_device->open_flag = old_flag;     } #else     rt_hw_console_output(rt_log_buf); #endif     va_end(args); }复制代码 如果使用设备驱动,则通过设备驱动函数将rt_log_buf缓冲区的内容输出到控制台。如果设备控制台打开失败则由rt_hw_console_output函数处理,这个函数需要用户单独实现。 本书只讲解如何将串口控制台重映射到rt_kprintf函数,rt_hw_console_output函数在board.c实现: /**   * [url=home.php?mod=space&uid=159083]@brief[/url]  重映射串口DEBUG_USARTx到rt_kprintf()函数   *   Note:DEBUG_USARTx是在bsp_usart.h中定义的宏,默认使用串口1   * @param  str:要输出到串口的字符串   * @retval 无   *   * @attention   *   */ void rt_hw_console_output(const char *str) {                 /* 进入临界段 */     rt_enter_critical();         /* 直到字符串结束 */     while (*str!='\0')         {                 /* 换行 */         if (*str=='\n')                 {                         USART_SendData(DEBUG_USART, '\r');                         while (USART_GetFlagStatus(DEBUG_USART, USART_FLAG_TXE) == RESET);                 }                 USART_SendData(DEBUG_USART, *str++);                                                  while (USART_GetFlagStatus(DEBUG_USART, USART_FLAG_TXE) == RESET);                 }                 /* 退出临界段 */     rt_exit_critical(); }复制代码 16 RT-Thread的启动流程 RT-Thread采用的是先启动初始线程,然后由初始线程来创建各种应用线程,然后再关闭初始线程的方法来启动。 并且在mian函数中稍做了修改。 当你拿到一个移植好的RT-Thread工程的时候,你去看main函数,只能在main函数里面看到创建线程和启动线程的代码,硬件初始化,系统初始化,启动调度器等信息都看不到。那是因为RT-Thread拓展了main函数,在main函数之前把这些工作都做好了。 我们知道,在系统上电的时候第一个执行的是启动文件里面由汇编编写的复位函数Reset_Handler,复位函数的最后会调用C库函数__main,__main函数的主要工作是初始化系统的堆和栈,最后调用C中的main函数,从而去到C的世界。 ; Reset handler Reset_Handler    PROC                  EXPORT  Reset_Handler             [WEAK]         IMPORT  SystemInit         IMPORT  __main                  LDR     R0, =SystemInit                  BLX     R0                  LDR     R0, =__main                  BX      R0                  ENDP复制代码 但当我们硬件仿真RT-Thread工程的时候,单步执行完__main之后,并不是跳转到C中的main函数,而是跳转到component.c中的$Sub$$main函数,因为RT-Thread使用编译器(这里仅讲解KEIL,IAR或者GCC稍微有点区别,但是原理是一样的)自带的$Sub$$和$Super$$这两个符号来扩展了main函数,使用$Sub$$main可以在执行main之前先执行$Sub$$main,在$Sub$$main函数中我们可以先执行一些预操作,当做完这些预操作之后最终还是要执行main函数,这个就通过调用$Super$$main来实现。当需要扩展的函数不是main的时候,只需要将main换成你要扩展的函数名即可,即$Sub$$function和$Super$$function。 就是在执行实际main函数值之前,可以执行$Sub$$main函数,然后采用$Super$$main回到main中, RT-Thread的硬件初始化是在components.c文件中的的$Sub$$main中完成的,因此实际的main函数中就没有硬件初始化相关的code,直接就开始了线程相关的操作。 硬件初始化相关code是在$Sub$$main的rtthread_startup();语句中完成的,这个语句调用startup函数: int rtthread_startup(void) {     rt_hw_interrupt_disable();     /* board level initalization      * NOTE: please initialize heap inside board initialization.      */     rt_hw_board_init();     /* show RT-Thread version */     rt_show_version();     /* timer system initialization */     rt_system_timer_init();     /* scheduler system initialization */     rt_system_scheduler_init(); #ifdef RT_USING_SIGNALS     /* signal system initialization */     rt_system_signal_init(); #endif     /* create init_thread */     rt_application_init();     /* timer thread initialization */     rt_system_timer_thread_init();     /* idle thread initialization */     rt_thread_idle_init();     /* start scheduler */     rt_system_scheduler_start();     /* never reach here */     return 0; } 这是main.c函数: /**   *********************************************************************   * @file    main.c   * @author  fire   * [url=home.php?mod=space&uid=252314]@version[/url] V1.0   * [url=home.php?mod=space&uid=311857]@date[/url]    2018-xx-xx   * @brief   RT-Thread 3.0 + STM32 工程模版   *********************************************************************   * @attention   *   * 实验平台:野火F429挑战者 STM32 开发板   * 论坛    :[url=http://www.firebbs.cn]http://www.firebbs.cn[/url]   * 淘宝    :[url=https://fire-stm32.taobao.com]https://fire-stm32.taobao.com[/url]   *   **********************************************************************   */ /* ************************************************************************* *                             包含的头文件 ************************************************************************* */ #include "board.h" #include "rtthread.h" /* ************************************************************************* *                               变量 ************************************************************************* */ /* 定义线程控制块 */ static rt_thread_t led1_thread = RT_NULL; /* ************************************************************************* *                             函数声明 ************************************************************************* */ static void led1_thread_entry(void* parameter); /* ************************************************************************* *                             main 函数 ************************************************************************* */ /**   * @brief  主函数   * @param  无   * @retval 无   */ int main(void) {             /*          * 开发板硬件初始化,RTT系统初始化已经在main函数之前完成,          * 即在component.c文件中的rtthread_startup()函数中完成了。          * 所以在main函数中,只需要创建线程和启动线程即可。          */                  led1_thread =                          /* 线程控制块指针 */     rt_thread_create( "led1",              /* 线程名字 */                       led1_thread_entry,   /* 线程入口函数 */                       RT_NULL,             /* 线程入口函数参数 */                       512,                 /* 线程栈大小 */                       3,                   /* 线程的优先级 */                       20);                 /* 线程时间片 */                        /* 启动线程,开启调度 */    if (led1_thread != RT_NULL)         rt_thread_startup(led1_thread);     else         return -1; } /* ************************************************************************* *                             线程定义 ************************************************************************* */ static void led1_thread_entry(void* parameter) {             while (1)     {         LED1_ON;         rt_thread_delay(500);   /* 延时500个tick */         rt_kprintf("led1_thread running,LED1_ON\r\n");                  LED1_OFF;              rt_thread_delay(500);   /* 延时500个tick */                                          rt_kprintf("led1_thread running,LED1_OFF\r\n");     } } /********************************END OF FILE****************************/ 复制代码 此内容由EEWORLD论坛网友传媒学子原创,如需转载或用于商业用途需征得作者同意并注明出处

  • 2019-05-06
  • 发表了主题帖: 【RT-Thread读书笔记】8. RT-Thread 学习13章读后感

    【RT-Thread读书笔记】8. RT-Thread 学习13章读后感第13章 移植RT-Thread到STM32 野火专门为此书配套了对应的程序,大家可以下载学习,配套书籍,可以理解的更深。 我们可以参照基于野火STM32全系列(包含M3/4/7)开发板的的RT-Thread的工程模板,让RT-Thread先跑起来。后续就是修修补补,所以初创性劳动非常费力,后续就是维护和升级了。 1.下载RT-Thread Nano 源码 Nano是Master的精简版,去掉了一些组件和各种开发板的BSP,保留了OS的核心功能,但足够我们使用。本书使用的是3.0.3版本。 RT-Thread Master的源码可从RT-Thread GitHub仓库地址:https://github.com/RT-Thread/rt-thread下载到,Nano就是从里面扣出来的。 另外也可以直接去keil官网上下载rt-thread nano的package. 建议是从github上下,或者用野火提供的代码。学习本书,尽量还是用野火给的rt-thread nano3.0.3来学习, 等熟练后可以自行搞master. 具体的添加过程,书中给了非常详尽的说明,这里不再赘述。 野火推荐我们直接将rt-thread代码拷贝到我们自己的工程中,方便release和维护。 一级文件夹                         二级文件夹                            描述 rtthread/3.0.3                      bsp                               板级支持包                                          components/finsh            RT-Thread组件                                          include                            头文件                                          include/libc                                          libcpu/arm/cortex-m0       与处理器相关的接口文件                                          libcpu/arm/cortex-m3                                          libcpu/arm/cortex-m4                                          libcpu/arm/cortex-m7                                           src                                RT-Thread内核源码 工程下我们可以按照下图建立: 然后,将RT-Thread/3.0.3/bsp文件夹下面的rtconfig.h配套文件拷贝到工程根目录下面的user文件夹,等下我们需要对这个文件进行修改。 用户可以通过修改这个RT-Thread内核的配置头文件来裁剪RT-Thread的功能,所以我们把它拷贝一份放在user这个文件夹下面。 拷贝board.c文件到user文件夹,需要修改。 bsp文件夹 里面存放的是板级支持包,即board support package的英文缩写。 如果为了减小工程的大小,bsp文件夹下面除了board.c和rtconfig.h这两个文件要保留外,其它的统统可以删除。 components文件夹 在RT-Thread看来,除了内核,其它第三方加进来的软件都是组件,比如gui、fatfs、lwip和finsh等。那么这些组件就放在components这个文件夹内,目前nano版本只放了finsh,其它的都被删除了,master版本则放了非常多的组件。finsh是RT-Thread组件里面最具特色的,它通过串口打印的方式来输出各种信息,方便我们调试程序。 include文件夹 目录下面存放的是RT-Thread内核的头文件,是内核不可分割的一部分。 libcpu文件夹简 存放的是硬件和软件之间的接口文件,涉及汇编和C,RT-Thread nano目前在libcpu目录下只放了cortex-m0、m3、m4和m7内核的单片机的接口文件,只要是使用了这些内核的mcu都可以使用里面的接口文件。 src文件夹 目录下面存放的是RT-Thread内核的源文件,是内核的核心。 ----------------------------------------------- 添加RT-Thread源码到工程组文件夹 rtt/source用于存放src文件夹的内容,rtt/ports用于存放libcpu/arm/cortex-m?文件夹的内容,“?”表示3、4或者7,具体选择哪个得看你使用的是野火哪个型号的STM32开发板 我用的是F429-挑战者STM32F429IGT6 因此RT-Thread不同内核的接口文件选libcpu/arm/cortex-m4。 指定RT-Thread头文件的路径 这个按需添加,在keil魔术棒中添加。 修改rtconfig.h rtconfig.h是直接从RT-Thread/3.0.3/bsp文件夹下面拷贝过来的,该头文件对裁剪整个RT-Thread所需的功能的宏均做了定义,有些宏定义被使能,有些宏定义被失能,一开始我们只需要配置最简单的功能即可。 其实就是一些注释,注释掉或者留下,书中写的很明确,也可以参照官网。 野火修改了:注释掉了 6 // #include "RTE_Components.h" 修改了: 12 #define RT_THREAD_PRIORITY_MAX 32 15 #define RT_TICK_PER_SECOND 1000 32 #define RT_MAIN_THREAD_STACK_SIZE 512 修改board.c 里面存放的是与硬件相关的初始化函数,按需修改。 添加core_delay.c和core_delay.h文件 只有在使用HAL库时才需要添加core_delay.c和core_delay.h文件。野火只在其M7系列的开发板使用了HAL,M4和M3使用的是标准库,不需要添加。我的不需要添加这些文件。 最后就是修改修改main.c 也没啥修改的,目前都是空的,创建线程是在第14章。这里就不在多说了,等明天分享第14章,感悟。 PS:大家还是看书,我这里也就是感悟笔记,把重点拎出来,Thanks 此内容由EEWORLD论坛网友传媒学子原创,如需转载或用于商业用途需征得作者同意并注明出处

  • 回复了主题帖: 【RT-Thread读书笔记】4. RT-Thread 学习6章读后感(一)

    沈婷婷 发表于 2019-5-6 14:02 我想问问你,创建线程是在单独的一个.c文件中吗?
    库建好了,在哪里都行。 给你举个野火的实例: /**   *********************************************************************   * @file    main.c   * @author  fire   * [url=home.php?mod=space&uid=252314]@version[/url] V1.0   * [url=home.php?mod=space&uid=311857]@date[/url]    2018-xx-xx   * [url=home.php?mod=space&uid=159083]@brief[/url]   RT-Thread 3.0 + STM32 工程模版   *********************************************************************   * @attention   *   * 实验平台:野火 F429挑战者 STM32 开发板   * 论坛    :[url]http://www.firebbs.cn[/url]   * 淘宝    :[url]https://fire-stm32.taobao.com[/url]   *   **********************************************************************   */ /* ************************************************************************* *                             包含的头文件 ************************************************************************* */ #include "board.h" #include "rtthread.h" /* ************************************************************************* *                               变量 ************************************************************************* */ /* 定义线程控制块 */ static rt_thread_t led1_thread = RT_NULL; /* ************************************************************************* *                             函数声明 ************************************************************************* */ static void led1_thread_entry(void* parameter); /* ************************************************************************* *                             main 函数 ************************************************************************* */ /**   * @brief  主函数   * @param  无   * @retval 无   */ int main(void) {            /*          * 开发板硬件初始化,RTT系统初始化已经在main函数之前完成,          * 即在component.c文件中的rtthread_startup()函数中完成了。          * 所以在main函数中,只需要创建线程和启动线程即可。          */                 led1_thread =                          /* 线程控制块指针 */     rt_thread_create( "led1",              /* 线程名字 */                       led1_thread_entry,   /* 线程入口函数 */                       RT_NULL,             /* 线程入口函数参数 */                       512,                 /* 线程栈大小 */                       3,                   /* 线程的优先级 */                       20);                 /* 线程时间片 */                        /* 启动线程,开启调度 */    if (led1_thread != RT_NULL)         rt_thread_startup(led1_thread);     else         return -1; } /* ************************************************************************* *                             线程定义 ************************************************************************* */ static void led1_thread_entry(void* parameter) {            while (1)     {         LED1_ON;         rt_thread_delay(500);   /* 延时500个tick */                  LED1_OFF;              rt_thread_delay(500);   /* 延时500个tick */                                     } } /********************************END OF FILE****************************/ 复制代码

  • 回复了主题帖: 学习有礼,分享也有礼!跟着小梅哥,一起intel SoC FPGA走起!

    看看能不能搞本教程,把开发板的灰清一清...

  • 回复了主题帖: 学习有礼,分享也有礼!跟着小梅哥,一起intel SoC FPGA走起!

    我就有一块 双arm硬核的FPGA,唉吃灰了,友晶给的资料不多,没学会呢..  很多人说zynq好,我还是习惯altera现在的intel风格...

  • 2019-05-05
  • 发表了主题帖: 【RT-Thread读书笔记】7. RT-Thread 学习8-12章读后感

    【RT-Thread读书笔记】7. RT-Thread 学习8-12章读后感 第8章 对象容器的实现第9章 空闲线程与阻塞延时的实现 第10章 支持多优先级 第11章 定时器的实现 第12章 支持时间片 --------------------------------------------------------------------------------------------------------------------------------------------------------------------- 第8章 对象容器的实现 在RT-Thread中,所有的数据结构都称之为对象。 在RTOS中,一般会有这些对象: 线程,信号量,互斥量、事件、邮箱、消息队列、内存堆、内存池、设备和定时器,这些在rtdef.h中有明显的枚举定义。 RT-Thread中,所有创建的对象都被放在容器中,这样做是为了方便管理和使用。 默认情况下,线程和定时器是必须在容器中存在的对象,其它的对象根据实际需要在宏定义中启用或者不用。 相关函数在 object.c中有叙述。 创建对象,首先需要初始化,然后将该对象插入到对象容器中。 代码如下: #include #include /* * 对象容器数组的下标定义,决定容器的大小 */ enum rt_object_info_type {     RT_Object_Info_Thread = 0,                         /* 对象是线程 */ #ifdef RT_USING_SEMAPHORE     RT_Object_Info_Semaphore,                          /* 对象是信号量 */ #endif #ifdef RT_USING_MUTEX     RT_Object_Info_Mutex,                              /* 对象是互斥量 */ #endif #ifdef RT_USING_EVENT     RT_Object_Info_Event,                              /* 对象是事件 */ #endif #ifdef RT_USING_MAILBOX     RT_Object_Info_MailBox,                            /* 对象是邮箱 */ #endif #ifdef RT_USING_MESSAGEQUEUE     RT_Object_Info_MessageQueue,                       /* 对象是消息队列 */ #endif #ifdef RT_USING_MEMHEAP     RT_Object_Info_MemHeap,                            /* 对象是内存堆 */ #endif #ifdef RT_USING_MEMPOOL     RT_Object_Info_MemPool,                            /* 对象是内存池 */ #endif #ifdef RT_USING_DEVICE     RT_Object_Info_Device,                             /* 对象是设备 */ #endif     RT_Object_Info_Timer,                              /* 对象是定时器 */ #ifdef RT_USING_MODULE     RT_Object_Info_Module,                             /* 对象是模块 */ #endif     RT_Object_Info_Unknown,                            /* 对象未知 */ }; #define _OBJ_CONTAINER_LIST_INIT(c)     \     {&(rt_object_container[c].object_list), &(rt_object_container[c].object_list)}                 static struct rt_object_information rt_object_container[RT_Object_Info_Unknown] = {     /* 初始化对象容器 - 线程 */     {         RT_Object_Class_Thread,         _OBJ_CONTAINER_LIST_INIT(RT_Object_Info_Thread),         sizeof(struct rt_thread)     },                 #ifdef RT_USING_SEMAPHORE     /* 初始化对象容器 - 信号量 */     {         RT_Object_Class_Semaphore,         _OBJ_CONTAINER_LIST_INIT(RT_Object_Info_Semaphore),         sizeof(struct rt_semaphore)     }, #endif                                #ifdef RT_USING_MUTEX     /* 初始化对象容器 - 互斥量 */     {         RT_Object_Class_Mutex,         _OBJ_CONTAINER_LIST_INIT(RT_Object_Info_Mutex),         sizeof(struct rt_mutex)     }, #endif                                #ifdef RT_USING_EVENT     /* 初始化对象容器 - 事件 */     {         RT_Object_Class_Event,         _OBJ_CONTAINER_LIST_INIT(RT_Object_Info_Event),         sizeof(struct rt_event)     }, #endif                 #ifdef RT_USING_MAILBOX     /* 初始化对象容器 - 邮箱 */     {         RT_Object_Class_MailBox,         _OBJ_CONTAINER_LIST_INIT(RT_Object_Info_MailBox),         sizeof(struct rt_mailbox)     }, #endif                         #ifdef RT_USING_MESSAGEQUEUE     /* 初始化对象容器 - 消息队列 */     {         RT_Object_Class_MessageQueue,         _OBJ_CONTAINER_LIST_INIT(RT_Object_Info_MessageQueue),         sizeof(struct rt_messagequeue)     }, #endif                                #ifdef RT_USING_MEMHEAP     /* 初始化对象容器 - 内存堆 */     {         RT_Object_Class_MemHeap,         _OBJ_CONTAINER_LIST_INIT(RT_Object_Info_MemHeap),         sizeof(struct rt_memheap)     }, #endif                                #ifdef RT_USING_MEMPOOL     /* 初始化对象容器 - 内存池 */     {         RT_Object_Class_MemPool,         _OBJ_CONTAINER_LIST_INIT(RT_Object_Info_MemPool),         sizeof(struct rt_mempool)     }, #endif                         #ifdef RT_USING_DEVICE     /* 初始化对象容器 - 设备 */     {         RT_Object_Class_Device,         _OBJ_CONTAINER_LIST_INIT(RT_Object_Info_Device), sizeof(struct rt_device)}, #endif     /* 初始化对象容器 - 定时器 */     /*     {         RT_Object_Class_Timer,         _OBJ_CONTAINER_LIST_INIT(RT_Object_Info_Timer),         sizeof(struct rt_timer)     },     */ #ifdef RT_USING_MODULE     /* 初始化对象容器 - 模块 */     {         RT_Object_Class_Module,         _OBJ_CONTAINER_LIST_INIT(RT_Object_Info_Module),         sizeof(struct rt_module)     }, #endif                 }; /** * 获取指定类型的对象信息 * * @param type 对象类似 * [url=home.php?mod=space&uid=784970]@return[/url] 对象信息 or RT_NULL */ struct rt_object_information *rt_object_get_information(enum rt_object_class_type type) {     int index;     for (index = 0; index < RT_Object_Info_Unknown; index ++)         if (rt_object_container[index].type == type) return &rt_object_container[index];     return RT_NULL; } /** * 该函数将初始化对象并将对象添加到对象容器中 * * @param object 要初始化的对象 * @param type 对象的类型 * @param name 对象的名字,在整个系统中,对象的名字必须是唯一的 */ void rt_object_init(struct rt_object         *object,                     enum rt_object_class_type type,                     const char               *name) {     register rt_base_t temp;     struct rt_object_information *information;     /* 获取对象信息,即从容器里拿到对应对象列表头指针 */     information = rt_object_get_information(type);     /* 设置对象类型为静态 */     object->type = type | RT_Object_Class_Static;     /* 拷贝名字 */     rt_strncpy(object->name, name, RT_NAME_MAX);     /* 关中断 */     temp = rt_hw_interrupt_disable();     /* 将对象插入到容器的对应列表中,不同类型的对象所在的列表不一样 */     rt_list_insert_after(&(information->object_list), &(object->list));     /* 使能中断 */     rt_hw_interrupt_enable(temp); } /** * 将对象从容器列表中脱离,但是对象占用的内存并不会释放 * * @param object 需要脱离容器的对象 */ void rt_object_detach(rt_object_t object) {     register rt_base_t temp;     /* 关中断 */     temp = rt_hw_interrupt_disable();     /* 从容器列表中脱离 */     rt_list_remove(&(object->list));     /* 开中断 */     rt_hw_interrupt_enable(temp); } /** * 判断一个对象释放是系统对象 * 通常,一个对象在初始化的时候会被设置为RT_Object_Class_Static * * @param 需要判断的对象类型 * * @return 如果是系统对象则返回RT_TRUE,否则则返回 RT_FALSE */ rt_bool_t rt_object_is_systemobject(rt_object_t object) {     if (object->type & RT_Object_Class_Static)         return RT_TRUE;     return RT_FALSE; }复制代码 第9章 空闲线程与阻塞延时的实现 1.空闲线程,就是没有任务需要执行时,执行的线程,有时候执行内存清理,有时候也可能进入低功耗状态。 2.阻塞延时,RTOS中的延时叫阻塞延时,即线程需要延时的时候,线程会放弃CPU的使用权,CPU可以去干其它的事情。 首先在RT-Thread中,idle.c与空闲线程有关,里面牵涉了定义空闲线程的栈,控制块,线程函数(初始化和其它功能函数)。 阻塞延时函数是在thread.c中定义的。 第10章 支持多优先级 前边讲了,就绪列表,线程切换,但是没有讲如何实现多优先级。 这一章节,主要讲了RT-Thread如何实现多优先级。 这个不难理解: RT-Thread要支持多优先级,需要靠就绪列表的支持,从代码上看,就绪列表由两个在scheduler.c文件定义的全局变量组成,一个是线程就绪优先级组rt_thread_ready_priority_group,另一个是线程优先级表rt_thread_priority_table[RT_THREAD_PRIORITY_MAX]。 0----- 最高优先级  可以挂载1 2 3 。。个任务---同一个优先级任务间采用时间片来决定运行时间的长短 1-----  次优先级 | 31----最低优先级,一般用来放空闲线程 第11章 定时器的实现 为了实现线程阻塞延时的精确控制,需要在每个线程控制块中内置一个定时器。 在RT-Thread中,每个线程都内置一个定时器,当线程需要延时的时候,则先将线程挂起,然后内置的定时器就会启动,并且将定时器插入到一个全局的系统定时器列表rt_timer_list,这个全局的系统定时器列表维护着一条双向链表,每个节点代表了正在延时的线程的定时器,节点按照延时时间大小做升序排列。当每次时基中断(SysTick中断)来临时,就扫描系统定时器列表的第一个定时器,看看延时时间是否到,如果到则让该定时器对应的线程就绪,如果延时时间不到,则退出扫描,因为定时器节点是按照延时时间升序排列的,第一个定时器延时时间不到期的话,那后面的定时器延时时间自然不到期。 定时器的实现是在timer.c中,涉及初始化函数,定时器结构体定义等内容。 这一章,讲解了如何在线程控制块中内置定时器,以及如何利用定时器实现阻塞延时。 不得不说RT-Thread这一点,做的还是很好的,相关代码值得深入研究,官网上相关介绍非常详尽,大家可以参考。 第12章 支持时间片 提到时间片,我记忆非常深刻,我有一次去面试嵌入式工程师,问及RTOS相关知识,当时也就用过freertos, 相关概念一窍不通,被问及线程如何切换,什么是时间片轮询,非常尴尬。感觉只会裸片编程,不会RTOS,简直就会被瞧不起一样。也正是那次面试之后,我下定决心要研究RTOS,因为linux实在是太大了,还是先弄明白一款RTOS,再研究Linux, 然后在学会如何搞嵌入式驱动。 火哥已经把时间片写的很明白了也很通俗了。 如下: 在RT-Thread中,当同一个优先级下有两个或两个以上线程的时候,线程支持时间片功能,即我们可以指定线程持续运行一次的时间,单位为tick。假如有两个线程分别为线程2和线程3,他们的优先级都为3,线程2的时间片为2,线程3的时间片为3。当执行到优先级为3的线程时,会先执行线程2,直到线程2的时间片耗完,然后再执行线程3。 到今天为止,紧紧巴巴把source code部分算是大概看完了,深感rt-thread之精妙,后续活动结束后,还得深入反复研究code,必将受益匪浅。 后续会陆续分享本书的第二部分,在野火STM32F4板子上,将RT-Thread这款RTOS用起来。 THANKS~ 此内容由EEWORLD论坛网友传媒学子原创,如需转载或用于商业用途需征得作者同意并注明出处

最近访客

< 1/4 >

统计信息

已有182人来访过

  • 芯币:2228
  • 好友:3
  • 主题:73
  • 回复:316
  • 课时:2
  • 资源:1

留言

你需要登录后才可以留言 登录 | 注册


warmeros 2018-11-25
你好,在么
查看全部