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开发、信号处理、工业通讯等技术主题。欢迎关注,一起交流,一起进步!
评论
  • 遇到部分串口工具不支持1500000波特率,这时候就需要进行修改,本文以触觉智能RK3562开发板修改系统波特率为115200为例,介绍瑞芯微方案主板Linux修改系统串口波特率教程。温馨提示:瑞芯微方案主板/开发板串口波特率只支持115200或1500000。修改Loader打印波特率查看对应芯片的MINIALL.ini确定要修改的bin文件#查看对应芯片的MINIALL.ini cat rkbin/RKBOOT/RK3562MINIALL.ini修改uart baudrate参数修改以下目
    Industio_触觉智能 2024-12-03 11:28 41浏览
  • 戴上XR眼镜去“追龙”是种什么体验?2024年11月30日,由上海自然博物馆(上海科技馆分馆)与三湘印象联合出品、三湘印象旗下观印象艺术发展有限公司(下简称“观印象”)承制的《又见恐龙》XR嘉年华在上海自然博物馆重磅开幕。该体验项目将于12月1日正式对公众开放,持续至2025年3月30日。双向奔赴,恐龙IP撞上元宇宙不久前,上海市经济和信息化委员会等部门联合印发了《上海市超高清视听产业发展行动方案》,特别提到“支持博物馆、主题乐园等场所推动超高清视听技术应用,丰富线下文旅消费体验”。作为上海自然
    电子与消费 2024-11-30 22:03 86浏览
  • 光伏逆变器是一种高效的能量转换设备,它能够将光伏太阳能板(PV)产生的不稳定的直流电压转换成与市电频率同步的交流电。这种转换后的电能不仅可以回馈至商用输电网络,还能供独立电网系统使用。光伏逆变器在商业光伏储能电站和家庭独立储能系统等应用领域中得到了广泛的应用。光耦合器,以其高速信号传输、出色的共模抑制比以及单向信号传输和光电隔离的特性,在光伏逆变器中扮演着至关重要的角色。它确保了系统的安全隔离、干扰的有效隔离以及通信信号的精准传输。光耦合器的使用不仅提高了系统的稳定性和安全性,而且由于其低功耗的
    晶台光耦 2024-12-02 10:40 102浏览
  • 作为优秀工程师的你,已身经百战、阅板无数!请先醒醒,新的项目来了,这是一个既要、又要、还要的产品需求,ARM核心板中一个处理器怎么能实现这么丰富的外围接口?踌躇之际,你偶阅此文。于是,“潘多拉”的魔盒打开了!没错,USB资源就是你打开新世界得钥匙,它能做哪些扩展呢?1.1  USB扩网口通用ARM处理器大多带两路网口,如果项目中有多路网路接口的需求,一般会选择在主板外部加交换机/路由器。当然,出于成本考虑,也可以将Switch芯片集成到ARM核心板或底板上,如KSZ9897、
    万象奥科 2024-12-03 10:24 37浏览
  • 概述 说明(三)探讨的是比较器一般带有滞回(Hysteresis)功能,为了解决输入信号转换速率不够的问题。前文还提到,即便使能滞回(Hysteresis)功能,还是无法解决SiPM读出测试系统需要解决的问题。本文在说明(三)的基础上,继续探讨为SiPM读出测试系统寻求合适的模拟脉冲检出方案。前四代SiPM使用的高速比较器指标缺陷 由于前端模拟信号属于典型的指数脉冲,所以下降沿转换速率(Slew Rate)过慢,导致比较器检出出现不必要的问题。尽管比较器可以使能滞回(Hysteresis)模块功
    coyoo 2024-12-03 12:20 70浏览
  • 当前,智能汽车产业迎来重大变局,随着人工智能、5G、大数据等新一代信息技术的迅猛发展,智能网联汽车正呈现强劲发展势头。11月26日,在2024紫光展锐全球合作伙伴大会汽车电子生态论坛上,紫光展锐与上汽海外出行联合发布搭载紫光展锐A7870的上汽海外MG量产车型,并发布A7710系列UWB数字钥匙解决方案平台,可应用于数字钥匙、活体检测、脚踢雷达、自动泊车等多种智能汽车场景。 联合发布量产车型,推动汽车智能化出海紫光展锐与上汽海外出行达成战略合作,联合发布搭载紫光展锐A7870的量产车型
    紫光展锐 2024-12-03 11:38 65浏览
  • 11-29学习笔记11-29学习笔记习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习笔记&记录学习习笔记&记学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&学习学习笔记&记录学习学习笔记&记录学习学习笔记&记
    youyeye 2024-12-02 23:58 51浏览
  • 《高速PCB设计经验规则应用实践》+PCB绘制学习与验证读书首先看目录,我感兴趣的是这一节;作者在书中列举了一条经典规则,然后进行详细分析,通过公式推导图表列举说明了传统的这一规则是受到电容加工特点影响的,在使用了MLCC陶瓷电容后这一条规则已经不再实用了。图书还列举了高速PCB设计需要的专业工具和仿真软件,当然由于篇幅所限,只是介绍了一点点设计步骤;我最感兴趣的部分还是元件布局的经验规则,在这里列举如下:在这里,演示一下,我根据书本知识进行电机驱动的布局:这也算知行合一吧。对于布局书中有一句:
    wuyu2009 2024-11-30 20:30 106浏览
  •         温度传感器的精度受哪些因素影响,要先看所用的温度传感器输出哪种信号,不同信号输出的温度传感器影响精度的因素也不同。        现在常用的温度传感器输出信号有以下几种:电阻信号、电流信号、电压信号、数字信号等。以输出电阻信号的温度传感器为例,还细分为正温度系数温度传感器和负温度系数温度传感器,常用的铂电阻PT100/1000温度传感器就是正温度系数,就是说随着温度的升高,输出的电阻值会增大。对于输出
    锦正茂科技 2024-12-03 11:50 66浏览
  • 最近几年,新能源汽车愈发受到消费者的青睐,其销量也是一路走高。据中汽协公布的数据显示,2024年10月,新能源汽车产销分别完成146.3万辆和143万辆,同比分别增长48%和49.6%。而结合各家新能源车企所公布的销量数据来看,比亚迪再度夺得了销冠宝座,其10月新能源汽车销量达到了502657辆,同比增长66.53%。众所周知,比亚迪是新能源汽车领域的重要参与者,其一举一动向来为外界所关注。日前,比亚迪汽车旗下品牌方程豹汽车推出了新车方程豹豹8,该款车型一上市就迅速吸引了消费者的目光,成为SUV
    刘旷 2024-12-02 09:32 98浏览
  • 学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习笔记&记录学习习笔记&记学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&
    youyeye 2024-11-30 14:30 70浏览
  • 艾迈斯欧司朗全新“样片申请”小程序,逾160种LED、传感器、多芯片组合等产品样片一触即达。轻松3步完成申请,境内免费包邮到家!本期热荐性能显著提升的OSLON® Optimal,GF CSSRML.24ams OSRAM 基于最新芯片技术推出全新LED产品OSLON® Optimal系列,实现了显著的性能升级。该系列提供五种不同颜色的光源选项,包括Hyper Red(660 nm,PDN)、Red(640 nm)、Deep Blue(450 nm,PDN)、Far Red(730 nm)及Ho
    艾迈斯欧司朗 2024-11-29 16:55 167浏览
  • RDDI-DAP错误通常与调试接口相关,特别是在使用CMSIS-DAP协议进行嵌入式系统开发时。以下是一些可能的原因和解决方法: 1. 硬件连接问题:     检查调试器(如ST-Link)与目标板之间的连接是否牢固。     确保所有必要的引脚都已正确连接,没有松动或短路。 2. 电源问题:     确保目标板和调试器都有足够的电源供应。     检查电源电压是否符合目标板的规格要求。 3. 固件问题: &n
    丙丁先生 2024-12-01 17:37 83浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦