switch之状态机爱你如初恋

嵌入式客栈 2021-02-26 00:00

(本文撰写于2021年情人节)


【说在前面的话】


在前面的一篇文章从零开始的状态机漫谈(1)——万物之始的语言中,我们介绍了状态机在整个计算机科学中宛如“世界基石”般的地位,同时介绍了一种“面向嵌入式环境”“高度简化”了的实用型状态图绘制方法——这里的“简化”是相对UML状态图的“繁杂”而言、且更接近课本上所使用的状态机图例;而这里的“实用”体现在:基于这套方法绘制的状态图是可以“无脑”而“严格”的翻译成C语言代码的


在展开后续内容之前,不得不为大家解释清楚一个非常具有 误导性的错误认知,即: 状态机天然是非阻塞(non-blocking)的,因而可以用于在裸机状态下实现多任务。实际上,这种说法后半段是正确的,错就错在前半部分,比如,就前一篇文章中所提到的一个状态图:

翻译成下面的C语言代码,在逻辑上毫无问题:
#include <stdbool.h>#include <stdint.h>
void print_hello(void) { //! 对应 start部分    uint8_t *s_pchSrc = "Hello";
do {            //! 对应 Print Hello 状态        while(!serial_out(*s_pchSrc));                //! serial_out返回值为true的状态迁移        s_pchSrc++;                //! 对应 "Is End of String"状态        if (*s_pchSrc == '\0') {            //! true分支,结束状态机            return ;        }        //! false分支,跳转到 "Print Hello" 状态    } while(true);}
怎么样?发现之前说法的错误之处了吧?——是的, 状态机(状态图)所描述的逻辑与翻译后的代码是否具有“非阻塞”的特性是无关的—— 翻译的方式不同,代码的特性也不同——但无论使用何种翻译方式,只要翻译是正确的,最终代码所对应的“状态机逻辑”就是“等效”的,比如,上面的状态机也可以翻译成如下的非阻塞形式:
#include <stdbool.h>#include <stdint.h>
typedef enum {    fsm_rt_err = -1,    fsm_rt_on_going = 0,    fsm_rt_cpl = 1,} fsm_rt_t;
#define PRINT_HELLO_RESET_FSM() \ do {s_tState = START;} while(0)
fsm_rt_t print_hello(void){ static enum { START = 0, PRINT_HELLO,        IS_END_OF_STRING, } s_tState = {START};        static const uint8_t *s_pchSrc = NULL;        switch (s_tState) {        case START:            //! 这个赋值写法只在嵌入式环境下“可能”是安全的            s_pchSrc = "Hello world";           s_tState++;         //break;        case PRINT_HELLO:            if (!serial_out(*s_pchSrc)) {             break;                     };            s_tState = IS_END_OF_STRING; s_pchSrc++;             //break;                    case IS_END_OF_STRING:            if (*s_pchSrc == '\0') {                PRINT_HELLO_RESET_FSM();                return fsm_rt_cpl;            } s_tState = PRINT_HELLO; break;                } return fsm_rt_on_going;}

对比两个代码,可以清楚的发现这两个事实:

  • 在状态机逻辑层面,两个代码都正确的翻译(表达)了状态图的逻辑

  • 在C代码的实际执行层面,一个是“不完成任务就绝不回来”的阻塞代码;一个是在状态执行间隙还会“悄悄”退出函数——释放处理器的非阻塞代码


所以说,与上述情况类似,市面上不少关于状态机的说法其实都是“有待商榷”、甚至是“错误的”,比如:

  • 状态机天然的是非阻塞代码;

  • 因为状态机经常切换,因此实时性好;

  • 状态机经常切换,没法以最快的速度响应事件,所以实时性差;

  • 状态机执行效率低下;

  • 状态机执行效率高;

  • 状态机占用代码空间大;

  • 状态机占用资源小,适合资源有限的小单片机;

  • 任何状态机都可以翻译成普通的RTOS任务(注意,这里的说法强调的不是不是状态机代码在RTOS任务里执行,而是把状态图翻译成RTOS任务)

  • ……


相信上述诸多误解和偏见中一定有一款是让你大为吃惊的。然而,如果你认为我这里列举出来的说法都是“错误的”,那么你就又错了


这里的要点是——以上说法并不是“非黑即白”的,而是来源于某一些具体的状态机翻译方式, 错就错在把某一种状态机翻译方式所具有的优点/缺点当成了整个状态机固有的优点/缺点——脱离了具体的状态机翻译方式,从而导致了“不准确”


说了这么多,无非就是想让你们知道以下几点:
  • 状态机/状态图的翻译方式众多;

  • 不同翻译方式在代码的行为特性上存在天壤之别;

  • 抛开具体翻译方式谈状态机特性都是耍流氓

  • 如果说状态图才是“新的源代码”,翻译C代码就是“新的汇编”,根据一定规则翻译状态图为C代码的过程就是”新的编译“。


下面我们就以大部分人第一次接触和使用状态机时常用的 switch 状态机为例,为大家介绍前一章所属状态图的翻译规则。

让我们上路吧!


(本文撰写于2021年情人节)


【状态函数返回值的“小心思”】


对很多人来说,即便状态机“初恋”不是使用 switch编写的函数,也一定逃不开使用函数作为状态机载体的形式(比如使用大量 if-else作为基础的状态机)。观察状态图,你会发现状态机是有返回状值的:

比如图中右上角的“on-going”和右下角的“cpl”,分别表示状态机“正在工作(on-going)”和“已经完成(complete)”。图上的状态机算是比较简单的了,其它状态机可能还有返回其它信息的需求——比如,一个接收字符的状态机可能还需要返回“超时(timeout)”这样的信息——因此,定义一个专门的枚举类型来作为状态机函数的返回值就显得非常有必要:

typedef enum { fsm_rt_on_going, fsm_rt_cpl,} fsm_rt_t;

到了这里,有一个细节问题需要考虑,fsm_rt_on_goingfsm_rt_cpl分别对应怎样的具体值好呢?(或者干脆不管?)。要解决这个问题,实际上只有是站在状态机函数用户角度考虑进行考虑,才能找到不会违反用户直觉(屁股决定脑袋)的答案。从状态机调用者的角度来看,既然我们告诉TA状态机函数是非阻塞的,那么用户最关心的最基本问题恐怕就是:


  • 状态机是否执行完成了?

  • 状态机有没有遇到什么自己不能处理的错误?


对于第一个问题,显然其答案是一个布尔量:

  • 如果返回false,则表明状态机还没有执行完成——需要继续执行(on-going);

  • 如果返回true,则表明状态机已经执行完成(complete)


基于这样的原因,完全可以根据 <stdbool.h> 中的定义,给我们的 fsm_rt_t 一个兼容的值,即:

typedef enum { fsm_rt_on_going = 0,    fsm_rt_cpl        = 1,} fsm_rt_t;

对于第二个问题,实际上,程序员之间有一个不成文的规定,即:错误码用负数表示,因此,我们可以引入一个“不问缘由的默认的错误码” (-1),并允许用户可以用除去(-1)以外的其它负数来编码更为具体的错误——这里就把这种自由度留给用户自己去发挥了,我们只需要在 fsm_rt_t 中引入(-1)就可以了:

typedef enum {    fsm_rt_err        = -1, fsm_rt_on_going = 0,    fsm_rt_cpl        = 1,} fsm_rt_t;

至此,我们完成了一个状态机返回值的定义过程,并隐含了以下的规则:

  • 对于“确定”不会返回错误码的状态机函数来说,状态机函数的使用与bool量是兼容的;

  • 用户可以使用负数来“自定义”错误码,并使用(-1)表示“不问缘由的默认错误码”;


需要特别强调的是,错误码表示发生了“状态机发生了预期之外、无法继续正常工作的情况”,比如,状态机函数需要一个指针,但你传了一个空指针;或是状态机函数收到了一个无效的输入参数,导致后续工作都无法正常执行,等等。


  • 用户定义的其它状态值,比如超时之类的,它们必须是大于(1)的正数。


与错误码不同,这类用返回值是状态机正常工作的结果,属于状态机逻辑本身所能预期和处理的。所以,哪怕“超时”听起来像是一个“错误”,但它本质上还是状态机逻辑所预期会发生并能正确检测和处理的,因此并不会作为一个负数错误码来返回。


在这个系列后面的文章中,我们还会引入两个默认的正整数状态返回值到 fsm_rt_t这里就先不赘述了:
//! \name finit state machine return value//! @{typedef enum { fsm_rt_err = -1, //!< fsm error, error code can be get from other interface fsm_rt_cpl = 0, //!< fsm complete fsm_rt_on_going = 1, //!< fsm on-going fsm_rt_wait_for_obj = 2, //!< fsm wait for object fsm_rt_asyn = 3, //!< fsm asynchronose mode, you can check it later.} fsm_rt_t;//! @}


借助 fsm_rt_t 类型的帮助,我们的状态机函数终于有了一个像样的外壳,比如:
fsm_rt_t <状态机函数的名字>([形参列表]){    ... return fsm_rt_on_going; //!< 默认的返回值}


为了方便大家的理解,我们就以“带超时功能的字符接收状态机”为例子,为大家介绍对应的状态图绘制方法以及对应的代码片段:

观察上图可以发现,状态机 read_byte会在读取字符的同时进行一个简单的倒计数;如果在 s_wCounter0之前成功读取到了一个字节,则返回 cplpchByte所指向的字节 buffer将保存对应的字节);如果读取字节失败,但计数器还未到零,则返回 on_going——表明状态机还在工作中;如果计数器到达了 0,则返回一个自定义的状态信息( timeout),用以表明发生了超时。在图中,不光矩形框内部多了一个名为 timeout 的黑色小圆点;在矩形框的外部(右侧)也出现了一个对应的扇出箭头,同样也标记了 timeout——这实际上是告诉我们, 当状态机迁移到 timeout 终点时,将通过 timeout 箭头扇出,而状态机也将复位


它对应的一个可能代码为:
enum {    fsm_rt_timeout = 4,     //!< 额外定义的状态返回值};
#ifndef TIMEOUT_CNT#   define TIMEOUT_CNT    (1000000ul)#endif
extern bool serial_in(uint8_t *pchByte);
#define READ_BYTE_RESET_FSM() \ do {s_tState = START;} while(0)fsm_rt_t read_byte(uint8_t *pchByte){ static enum { START = 0, READ_BYTE, IS_TIMEOUT, } s_tState = {START}; static uint32_t s_wCounter; if (NULL == pchByte) { READ_BYTE_RESET_FSM();        return fsm_rt_err;   //!< 检测到无效的输入参数 }
    switch (s_tState) {     case START: s_wCounter = TIMEOUT_CNT; s_tState++; //break;        case READ_BYTE:         if (serial_in(pchByte)) {                READ_BYTE_RESET_FSM();                return fsm_rt_cpl;         }                        s_wCounter--;            s_tState = IS_TIMEOUT;            //break;                    case IS_TIMEOUT:         if (0 == s_wCounter) {         READ_BYTE_RESET_FSM();         return (fsm_rt_t) fsm_rt_timeout;         }         s_tState = READ_BYTE;            break;    }         return fsm_rt_on_going;}

这个代码有几个细节值得大家注意:

  • fsm_rt_timeout 是一个额外定义的枚举,其实我们并不需要给它配备一个所谓的类型——毕竟只是拿它当一个常数用,直接用匿名枚举就行了;

  • fsm_rt_timeout 本质上是属于匿名枚举的,因此作为兼容 fsm_rt_t 的值返回时,有些编译器还是会报告 warning——提示我们返回值并不是 fsm_rt_t 的一部分——这里我们直接使用强制类型转换让编译器“闭嘴即可”;

  • 状态函数需要用户传入一个指针 pchByte,容易发现,如果传入值是NULL,整个状态机就无法正常工作了,因而视作错误,需要返回负数错误码;又由于这里我们很懒,没有定义专门定义这一情况的错误码,因此以 fsm_rt_err 来凑数。一般来说错误码的返回值是不用在状态图上进行明确标注的


【不要小看了状态的定义】


与返回值类似,状态机的状态也可以用枚举来定义,但这里有一些细节是需要注意的:
  • 由于定义状态的枚举实际上是状态机函数的“私有财产”,也就是说只有状态机函数会“使用且只用一次”,因此:

    • 没有必要为其使用 typedef 来定义一个类型;

    • 应该放在状态机函数的内部——由花括号限制枚举的作用范围;

    • 由于这一枚举类型的作用范围被限制在了函数内部,因此状态机之间不存在“重名”或者“命名空间污染”的问题——换句话说,

      • 每个状态的名称都可以尽可能的简单;

      • START在每个状态机函数里都可以被定义一次,而且永远叫START

  • 状态的命名上应该尽可能以状态图上的状态名为“蓝本”;

  • 状态名应该尽可能的有意义,而不是像STATE_ASTATE_B, ... STATE_X 这样“用一个英文字母序号”去代表“0,1,2...n这样的数字序号”——二者无论是谁都没有为“状态是做什么的”提供任何有意义的信息。相对的,例如 READ_BYTEIS_TIMEOUT 这样的名称就非常简洁明了。


以前面read_byte状态机代码为例,一些错误的或者说不推荐的做法为:

//!< 错误一:只用一次的枚举,没必要定义类型//!< 错误二:这个枚举是 read_byte 的私有财产,应该放到函数内部typedef enum {    FSM_RB_START = 0,  //!< 不推荐一:没必要加前缀    FSM_RB_STATE_A,    //!< 不推荐二:用字母序号替代数字序号,脱裤子放屁,完全没提供任何有意义的信息 FSM_RB_STATE_B,} read_byte_state_t;
fsm_rt_t read_byte(uint8_t *pchByte){ static read_byte_state_t s_tState = {FSM_RB_START};    ...}

作为对比,正确的做法如下:

fsm_rt_t read_byte(uint8_t *pchByte){ static enum { START = 0, READ_BYTE, IS_TIMEOUT, } s_tState = {START};    ...}


【START不是状态】


如果你认真阅读 《从零开始的状态机漫谈(1)——万物之始的语言并观察状态图会发现:START是状态机的起点、同时也兼任跃迁条件——换句话说:
  • START 不是一个可以保持的状态,它也不能被看作一个特殊的状态;因此,翻译代码的时候,虽然START是0,但在对应的case分支中,一定要自动切换到下一个状态而绝对不能在此停留——这就是纪律!

  • 另外一个“START不能被当做状态来使用”的原因是,start作为一个跃迁条件,它是可以拥有“发生跃迁时执行且只执行一次的动作的”——又由于START是处于复位状态的状态机第一次执行时的起点,因此START所携带的执行动作一般用作状态机的初始化——比如初始化状态机所使用的变量等等

  • 如果状态机需要动态申请资源,比如malloc,考虑到失败的可能,如果允许重试,则这类资源分配代码就不能放置在START中,因为我们说过,START不是状态——在状态机复位之前不应该重复执行;如果分配失败被视作错误,会返回负数的错误码,并复位状态机,则允许将这类资源分配代码放置到START中——因为逻辑上我们遵守了规则。


作为例子,不要尝试干出这种事情:

fsm_rt_t example(...){ static enum { START = 0, ...
} s_tState = {START};    static uint32_t s_pchArray;

    switch (s_tState) {     case START: s_pchArray = malloc(64); if (NULL == s_pchArray) { break; } s_tState++;    ...}



应该专门给这类允许重试的资源分配一个独立的状态:


fsm_rt_t example(...){ static enum { START = 0,        MALLOC, ... } s_tState = {START};    static uint32_t s_pchArray;
    switch (s_tState) {     case START:            s_tState++;            //break;        case MALLOC: s_pchArray = malloc(64); if (NULL == s_pchArray) { break; } s_tState = XXXXX; break;     ...}


【如何实现从状态到代码的“无脑翻译”】


经过了这么多的准备工作,我们终于进入到具体状态的翻译这一环节中了。事实上,状态的翻译比你想象的要简单,针对下面的一个状态示意图:

它可以简单的对应到下面的代码结构:

 case <状态名称>:        状态具体执行了什么有返回值d的动作;        if (返回值 满足 跃迁条件1) {            s_tState = XXXXX;   //!< 执行状态跃迁            执行对应的跃迁动作        } else if (返回值 满足 跃迁条件2) { s_tState = XXXXX; //!< 执行状态跃迁 执行对应的跃迁动作                }        break;

一般来说,我们既可以用上面的公式无脑翻译代码,也可以进行必要的等效改编。比如,对于READ_BYTE状态:

我们可以无脑翻译成如下的代码:

        case READ_BYTE:         if (serial_in(pchByte)) {                READ_BYTE_RESET_FSM();                return fsm_rt_cpl;         }                        s_wCounter--;            s_tState = IS_TIMEOUT;            break;  


如果我在这里说,状态的翻译并不复杂,一些小伙伴可能会“哼”的冷笑一声,顺手甩出一个“王炸”——“ 如果一个状态很复杂怎么办”?对于这个问题,我的答案是:
  • 如果你的状态很复杂,那么一定可以拆分成多个状态彼此配合的形式;

  • 拆分后每个状态都应该功能单一;

  • 拆分后的逻辑应该更加清晰;


所以,不要问我“一个状态很复杂怎么翻译”,先看看你是不是做了所谓的“超级状态”——尝试把很多事情都在一个状态里做了——如果发生了这种事情,请反思这跟“把所有应用代码都写在超级循环里,而且还不涉及函数调用”有啥区别。最后,关于把“超级状态”拆分成多个简单状态的组合以后可能面临的“所谓”性能优化问题,我们将在本系列后面的文章从零开始的状态机漫谈(3)——状态机设计原则:清晰!清晰!还是清晰!为您详细介绍,敬请期待。


【复位是一门大学问】


读到这里,很多小伙伴可能已经在前面的代码中发现了如下的细节:
#define READ_BYTE_RESET_FSM() \ do {s_tState = START;} while(0)

或是:

#define PRINT_HELLO_RESET_FSM() \ do {s_tState = START;} while(0)

于是心中升起了疑问:如果复位就是把状态变量重新设置为 START

  • 为什么不直接在图上所有要复位的地方直接画一条箭头——跃迁到到第一状态?

  • 为什么不定一个统一的宏,比如叫 RESET_FSM() 就好了,而是给每个状态机都定义一个自己的宏?


要回答第一个问题并不困难:

  • 复位并不是普通的状态跃迁,它表示将状态机“重置”——复位后的第一次执行,状态机会从START那里开始,并且完成必要的状态机初始化操作;

  • 统一采用START作为状态机的起点,可以避免第一个状态出现恐怖数量的扇入箭头,从而极大的简化了状态图(你也不想看到蜘蛛网一样密集的箭头吧);

  • 避免了每个扇入的跃迁所拥有的“初始化代码”可能会存在“不同”而导致的代码陷阱——因为我们统一从START进入,因此只要维护一份初始化代码就足够了。


对于第二个问题,我们要从更长远的角度来考虑:现阶段的状态机也许很简单,所以复位仅仅是重置状态变量就够了;然而,随着应用结构的复杂,以及状态机翻译方式的改进或者变化,每个状态机函数所需的复位操作可能都是不同的,因此从养成好习惯的角度出发,应该给每一个状态机都配备一个专属的复位宏


很多小伙伴在编写状态机的时候,可能会有这样一类要求:即, 出于某种原因,应用程序的某些模块需要“从外部”复位某些状态机,换句话说——就是杀死状态机——这其实很类似RTOS里面,杀死某个任务线程的情况。对此我要说说我的看法:
  • 首先,应该尽最大可能避免从状态机外部复位状态机,或者说,状态机的生命周期应该掌控在自己手里。这么做的原因很简单,也很关键,即理论上没有任何人比状态机自己更清楚如何安全而有效的复位一个状态机。如果这么说你不能理解,考虑如下几种情况:

    • 状态机中可能存在动态分配的资源,状态机自己内部的复位过程中会正确的释放这些资源;而来自外部的“它杀”在杀手掌握的信息不充分的情况下,可能会导致这类资源未被正确释放

    • 状态机非常适合用作各类机械控制,然而,出于机械机构的特殊原因,为了防止损害设备,或者伤害到人员,这类状态机都会根据当前的工作状态,有一套针对性的(通常是不同的)复位序列(甚至某些状态下根本不允许复位),而且复位过程本身也是需要时间的,因此在这种情况下直接由外部进行“他杀”实际上是不可承受之重

  • 其次,应该用“自杀请求”来代替“直接他杀”,即:状态机在设计时即提供一个“复位请求”信号,并在状态机内部适当的状态检测这一信号;外部应用只能通过这一信号来“请求”状态机复位;当复位成功后,状态机应该通过某种手段,比如特定的返回值或者回调函数来告知请求者“复位完成”。



【细数那些绝对要杜绝的“骚操作”】


在设计状态机或者翻译 switch状态机的过程中,以下常见“骚操作”是应该避免的:
  • 在一个函数里塞入多个switch状态机实现——请记住,每个switch状态机都应该有自己专属的一个函数; 

  • 在 switch 外部添加各类功能性的代码。这种做法,本质上就是模拟了“多线程”,也就是switch状态机逻辑被看作一个“线程”、switch外部的功能代码客观上就充当了另外一个“线程”。这种情况完全可以通过将两份代码拆分到独立的任务函数中,并以某种形式的“任务间通信”完成协调——最终实现一样的功能

  • 把状态变量定义到状态机函数外部,从而方便别人“偷窥”或者“复位”——请参考前面一个章节的内容,用“自杀”替代“它杀”。



【后记】


相信对很多人来说, switch状态机都是它们裸机环境下的“制胜法宝”,我并不准备否认这一点,相反,我希望通过这篇文章,能够分享一下我在使用 switch方式翻译状态图的一些做法以及背后的思考。
希望大家不要误解我——认为我这里介绍的方法就是 switch 状态机编写方式的“权威”,很遗憾的是,如果你有这种想法,那么我在本文开头处所作的努力就化为乌有了—— 也许状态图的所表达的逻辑是唯一的,但翻译它的方法从来都不是唯一的;同时 每一个方法都有自己的利弊,希望大家在讨论喜好的时候,不要动辄就把某一类方法的特点强加到“状态机”整体身上加以评判。




原创不易,

如果你喜欢我的思维、觉得我的文章对你有所启发,

请务必 “点赞、收藏、转发” 三连,这对我很重要!谢谢!


欢迎订阅 裸机思维



嵌入式客栈 欢迎关注嵌入式客栈,主要分享嵌入式Linux系统构建、嵌入式linux驱动开发、单片机技术、FPGA开发、信号处理、工业通讯等技术主题。欢迎关注,一起交流,一起进步!
评论
  • 本文介绍编译Android13 ROOT权限固件的方法,触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。关闭selinux修改此文件("+"号为修改内容)device/rockchip/common/BoardConfig.mkBOARD_BOOT_HEADER_VERSION ?= 2BOARD_MKBOOTIMG_ARGS :=BOARD_PREBUILT_DTB
    Industio_触觉智能 2025-01-08 00:06 84浏览
  • 这篇内容主要讨论三个基本问题,硅电容是什么,为什么要使用硅电容,如何正确使用硅电容?1.  硅电容是什么首先我们需要了解电容是什么?物理学上电容的概念指的是给定电位差下自由电荷的储藏量,记为C,单位是F,指的是容纳电荷的能力,C=εS/d=ε0εrS/4πkd(真空)=Q/U。百度百科上电容器的概念指的是两个相互靠近的导体,中间夹一层不导电的绝缘介质。通过观察电容本身的定义公式中可以看到,在各个变量中比较能够改变的就是εr,S和d,也就是介质的介电常数,金属板有效相对面积以及距离。当前
    知白 2025-01-06 12:04 209浏览
  • 村田是目前全球量产硅电容的领先企业,其在2016年收购了法国IPDiA头部硅电容器公司,并于2023年6月宣布投资约100亿日元将硅电容产能提升两倍。以下内容主要来自村田官网信息整理,村田高密度硅电容器采用半导体MOS工艺开发,并使用3D结构来大幅增加电极表面,因此在给定的占位面积内增加了静电容量。村田的硅技术以嵌入非结晶基板的单片结构为基础(单层MIM和多层MIM—MIM是指金属 / 绝缘体/ 金属) 村田硅电容采用先进3D拓扑结构在100um内,使开发的有效静电容量面积相当于80个
    知白 2025-01-07 15:02 139浏览
  • 在智能家居领域中,Wi-Fi、蓝牙、Zigbee、Thread与Z-Wave等无线通信协议是构建短距物联局域网的关键手段,它们常在实际应用中交叉运用,以满足智能家居生态系统多样化的功能需求。然而,这些协议之间并未遵循统一的互通标准,缺乏直接的互操作性,在进行组网时需要引入额外的网关作为“翻译桥梁”,极大地增加了系统的复杂性。 同时,Apple HomeKit、SamSung SmartThings、Amazon Alexa、Google Home等主流智能家居平台为了提升市占率与消费者
    华普微HOPERF 2025-01-06 17:23 195浏览
  • 根据环洋市场咨询(Global Info Research)项目团队最新调研,预计2030年全球无人机锂电池产值达到2457百万美元,2024-2030年期间年复合增长率CAGR为9.6%。 无人机锂电池是无人机动力系统中存储并释放能量的部分。无人机使用的动力电池,大多数是锂聚合物电池,相较其他电池,锂聚合物电池具有较高的能量密度,较长寿命,同时也具有良好的放电特性和安全性。 全球无人机锂电池核心厂商有宁德新能源科技、欣旺达、鹏辉能源、深圳格瑞普和EaglePicher等,前五大厂商占有全球
    GIRtina 2025-01-07 11:02 115浏览
  • By Toradex 秦海1). 简介嵌入式平台设备基于Yocto Linux 在开发后期量产前期,为了安全以及提高启动速度等考虑,希望将 ARM 处理器平台的 Debug Console 输出关闭,本文就基于 NXP i.MX8MP ARM 处理器平台来演示相关流程。 本文所示例的平台来自于 Toradex Verdin i.MX8MP 嵌入式平台。  2. 准备a). Verdin i.MX8MP ARM核心版配合Dahlia载板并
    hai.qin_651820742 2025-01-07 14:52 101浏览
  • 本文介绍Linux系统更换开机logo方法教程,通用RK3566、RK3568、RK3588、RK3576等开发板,触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。制作图片开机logo图片制作注意事项(1)图片必须为bmp格式;(2)图片大小不能大于4MB;(3)BMP位深最大是32,建议设置为8;(4)图片名称为logo.bmp和logo_kernel.bmp;开机
    Industio_触觉智能 2025-01-06 10:43 92浏览
  • 大模型的赋能是指利用大型机器学习模型(如深度学习模型)来增强或改进各种应用和服务。这种技术在许多领域都显示出了巨大的潜力,包括但不限于以下几个方面: 1. 企业服务:大模型可以用于构建智能客服系统、知识库问答系统等,提升企业的服务质量和运营效率。 2. 教育服务:在教育领域,大模型被应用于个性化学习、智能辅导、作业批改等,帮助教师减轻工作负担,提高教学质量。 3. 工业智能化:大模型有助于解决工业领域的复杂性和不确定性问题,尽管在认知能力方面尚未完全具备专家级的复杂决策能力。 4. 消费
    丙丁先生 2025-01-07 09:25 108浏览
  • 故障现象一辆2017款东风风神AX7车,搭载DFMA14T发动机,累计行驶里程约为13.7万km。该车冷起动后怠速运转正常,热机后怠速运转不稳,组合仪表上的发动机转速表指针上下轻微抖动。 故障诊断 用故障检测仪检测,发动机控制单元中无故障代码存储;读取发动机数据流,发现进气歧管绝对压力波动明显,有时能达到69 kPa,明显偏高,推断可能的原因有:进气系统漏气;进气歧管绝对压力传感器信号失真;发动机机械故障。首先从节气门处打烟雾,没有发现进气管周围有漏气的地方;接着拔下进气管上的两个真空
    虹科Pico汽车示波器 2025-01-08 16:51 51浏览
  • 彼得·德鲁克被誉为“现代管理学之父”,他的管理思想影响了无数企业和管理者。然而,关于他的书籍分类,一种流行的说法令人感到困惑:德鲁克一生写了39本书,其中15本是关于管理的,而其中“专门写工商企业或为企业管理者写的”只有两本——《为成果而管理》和《创新与企业家精神》。这样的表述广为流传,但深入探讨后却发现并不完全准确。让我们一起重新审视这一说法,解析其中的矛盾与根源,进而重新认识德鲁克的管理思想及其著作的真正价值。从《创新与企业家精神》看德鲁克的视角《创新与企业家精神》通常被认为是一本专为企业管
    优思学院 2025-01-06 12:03 152浏览
  • 每日可见的315MHz和433MHz遥控模块,你能分清楚吗?众所周知,一套遥控设备主要由发射部分和接收部分组成,发射器可以将控制者的控制按键经过编码,调制到射频信号上面,然后经天线发射出无线信号。而接收器是将天线接收到的无线信号进行解码,从而得到与控制按键相对应的信号,然后再去控制相应的设备工作。当前,常见的遥控设备主要分为红外遥控与无线电遥控两大类,其主要区别为所采用的载波频率及其应用场景不一致。红外遥控设备所采用的射频信号频率一般为38kHz,通常应用在电视、投影仪等设备中;而无线电遥控设备
    华普微HOPERF 2025-01-06 15:29 160浏览
  •  在全球能源结构加速向清洁、可再生方向转型的今天,风力发电作为一种绿色能源,已成为各国新能源发展的重要组成部分。然而,风力发电系统在复杂的环境中长时间运行,对系统的安全性、稳定性和抗干扰能力提出了极高要求。光耦(光电耦合器)作为一种电气隔离与信号传输器件,凭借其优秀的隔离保护性能和信号传输能力,已成为风力发电系统中不可或缺的关键组件。 风力发电系统对隔离与控制的需求风力发电系统中,包括发电机、变流器、变压器和控制系统等多个部分,通常工作在高压、大功率的环境中。光耦在这里扮演了
    晶台光耦 2025-01-08 16:03 44浏览
  • 根据Global Info Research项目团队最新调研,预计2030年全球封闭式电机产值达到1425百万美元,2024-2030年期间年复合增长率CAGR为3.4%。 封闭式电机是一种电动机,其外壳设计为密闭结构,通常用于要求较高的防护等级的应用场合。封闭式电机可以有效防止外部灰尘、水分和其他污染物进入内部,从而保护电机的内部组件,延长其使用寿命。 环洋市场咨询机构出版的调研分析报告【全球封闭式电机行业总体规模、主要厂商及IPO上市调研报告,2025-2031】研究全球封闭式电机总体规
    GIRtina 2025-01-06 11:10 117浏览
  • 「他明明跟我同梯进来,为什么就是升得比我快?」许多人都有这样的疑问:明明就战绩也不比隔壁同事差,升迁之路却比别人苦。其实,之间的差异就在于「领导力」。並非必须当管理者才需要「领导力」,而是散发领导力特质的人,才更容易被晓明。许多领导力和特质,都可以通过努力和学习获得,因此就算不是天生的领导者,也能成为一个具备领导魅力的人,进而被老板看见,向你伸出升迁的橘子枝。领导力是什么?领导力是一种能力或特质,甚至可以说是一种「影响力」。好的领导者通常具备影响和鼓励他人的能力,并导引他们朝着共同的目标和愿景前
    优思学院 2025-01-08 14:54 47浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦