基于FlexCAN适配CANopenNode

准备微控制器基本工程在微控制器上适配canopennode配置电路板的时钟和引脚 board_init.c准备硬件定时器 main.c对接can驱动 co_driver.c & main.c本文是《can总线开发一本全(6) - canopennode组件》的补充其中一个小节。
总结在微控制器平台上移植canopennode,需要根据具体硬件条件,适配2个源文件:
canopennode-1.3/stack/drvtemplate/co_driver.c 文件。补充co_canmodule_init() 函数:初始化can外设硬件,配置can协议引擎、收发报文消息的参数,以及启用中断。补充co_cansend() 函数:复制canopennode组件中缓冲区的消息帧到硬件引擎,交由can硬件外设发送到总线上。补充co_caninterrupt() 函数:由硬件的can中断服务调用,实现硬件外设层面上的发送和接收can通信帧。补充co_canverifyerrors() 函数:上报硬件can外设模块的检测到的错误状态。canopennode-1.3/example/main.c 文件。创建并配置硬件定时器周期中断服务,以1ms为周期,调用canopennode的定时器周期执行线程的函数。接下来,将以集成了flexcan外设模块的mm32f0140微控制器为例,实现对canopennode v1.3的适配过程。
目前灵动官方的软件开发平台mindsdk已经适配了canopennode协议栈,并创建了一系列样例工程:
co_basicco_pdo_masterco_pdo_slaveco_sdo_masterco_sdo_slaveco_with_eepromco_with_flash为了描述适配canopennode的过程,这里仍然从零开始,展现完整的移植开发过程。
准备微控制器基本工程首先从灵动mindsdk的网站上(https://mindsdk.mindmotion.com.cn/)获取到pokt-f0140(使用mm32f0140主控)开发板的flexcan驱动样例工程,flexcan_loopback,作为模板工程。这个模板工程里包含了mm32f0140微控制器正常工作的所有必要源码,包括芯片头文件、启动程序、中断向量表、以及一系列初始化硬件电路板到进入main()函数的源码,以及flexcan外设模块的驱动程序。
将模板工程的工程名改为fthr-f1040_canopen_demo_mdk将canopennode组件的源码包canopennode-v1.3复制到canopen_demo工程的根目录下将其中stack/drvtemplate目录下的co_driver.c和co_driver_target.h文件复制到canopen_demo工程的board目录下将其中example目录下的的co_od.c、co_od.h和main.c文件复制到canopen_demo工程的application目录下在keil mdk环境中打开canopen_demo工程。添加源文件和包含路径到工程中,如图x所示。
添加canopennode-v1.3目录下,canopennode-v1.3/stack目录下所有的c源文件到工程添加canopennode-v1.3目录和canopennode-v1.3/stack目录到工程包含路径添加application目录下新增文件co_od.c、co_od.h和main.c,和board目录下新增文件co_driver.c和co_driver_target.h,到canopen_demo工程中。
figure-canopen-demo-proj-settings
图x canopen_demo工程中包含canopennode源码整理好文件之后,试着编译一下工程,没有警告和错误,可以正常使用。如图x所示。
figure-canopen-demo-proj-build-log
图x canopen_demo工程编译正确此时的canopen_demo工程中,包含了canopennode的所有源码、flexcan外设模块的驱动,以及使用mm32f0140微控制器的所有必要的源文件,并且可以通过编译器验证编写程序代码的正确性。后续进行适配工作过程中,将通过开发者自行编码,将canopennode和flexcan外设模块绑定起来,并可实时编译工程验证编码内容。
在微控制器上适配canopennodecanopeonode组件中自带main.c文件,约定了整个canopen协议栈的运行框架。在canopennode中的main.c文件中,定义了应用程序入口main()函数,以及定时器中断服务程序入口和can外设模块中断服务程序入口。在本例的移植工程中,定时器相关的程序被置于main.c文件中,而具体微控制器平台上的can外设模块相关的配置程序代码则位于co_driver.c文件中。
配置电路板的时钟和引脚 board_init.c配置时钟这里需要至少启用硬件定时器tim2模块(产生1ms周期中断)和flexcan模块,另外,pokt-f0140开发板使用pb8和pb9作为can接口引脚,也需要启用对应io端口的时钟。
在clock_init.c文件中更新board_initbootclocks()源码:
void board_initbootclocks(void){ clock_resettodefault(); clock_boottohse72mhz(); /* tim2.*/ rcc_enableapb1periphs(rcc_apb1_periph_tim2, true); rcc_resetapb1periphs(rcc_apb1_periph_tim2); /* flexcan. */ rcc_enableapb1periphs(rcc_apb1_periph_flexcan, true); rcc_resetapb1periphs(rcc_apb1_periph_flexcan); ... /* gpiob. */ rcc_enableahb1periphs(rcc_ahb1_periph_gpiob, true); /* pb8 - can1_rx, pb9 - can1_tx. */ rcc_resetahb1periphs(rcc_ahb1_periph_gpiob);}配置引脚fthr-f0140开发板使用pb8和pb9作为can接口引脚,需要配置引脚的复用功能为can服务。
在pin_init.c文件中更新board_initpins()源码:
void board_initpins(void){ ... /* fthr-f0140. */ /* pa9 - flexcan_rx. */ gpio_init.pins = gpio_pin_9; gpio_init.pinmode = gpio_pinmode_in_floating; gpio_init.speed = gpio_speed_50mhz; gpio_init(gpioa, &gpio_init); gpio_pinafconf(gpioa, gpio_init.pins, gpio_af_8); /* pa10 - flexcan_tx. */ gpio_init.pins = gpio_pin_10; gpio_init.pinmode = gpio_pinmode_af_pushpull; gpio_init.speed = gpio_speed_50mhz; gpio_init(gpioa, &gpio_init); gpio_pinafconf(gpioa, gpio_init.pins, gpio_af_8);}配置板子的board_initbootclocks()函数和board_initpins()函数,将在board_init.c文件中被board_init()函数调用,
void board_init(void){ board_initbootclocks(); board_initpins(); board_initdebugconsole();}board_init()函数最终将在main.c文件中被调用,实现对电路板的初始化工作。
/* main ***********************************************************************/int main (void){ co_nmt_reset_cmd_t reset = co_reset_not; /* configure microcontroller. */ board_init(); ...}准备硬件定时器 main.ccanopennode的三个线程之一,定时器周期执行线程,以1ms为间隔周期执行。例如,可以配置硬件定时器tim2产生周期为1ms的中断服务,并在定时器的中断服务程序中嵌入canopennode提供main.c文件中的tmrtask_thread()函数。
在main.c文件中编写brd_tim_init()函数,配置tim2硬件定时器,并在main()函数中调用:
#include board_init.hvoid board_tim_init(void);/* main ***********************************************************************/int main (void){ ... /* configure timer interrupt function for execution every 1 millisecond */ board_tim_init(); ...}/* setup the hardware timer. */void board_tim_init(void){ /* set the counter counting step. */ tim_init_type tim_init; tim_init.clockfreqhz = board_tim_freq; tim_init.stepfreqhz = board_tim_update_step; /* 1ms. */ tim_init.period = board_tim_update_period - 1u; tim_init.enablepreloadperiod = false; tim_init.periodmode = tim_periodmode_continuous; tim_init.countmode = tim_countmode_increasing; tim_init(board_tim_port, &tim_init); /* enable interrupt. */ tim_enableinterrupts(board_tim_port, tim_int_update_period, true); nvic_enableirq(board_tim_irqn); /* start the timer. */ tim_start(board_tim_port);}其中,关于硬件定时器的配置参数的定义统一放置于board_init.h文件。
/* tim1. */#define board_tim_port (tim_type *)tim2#define board_tim_irqn tim2_irqn#define board_tim_irqhandler tim2_irqhandler#define board_tim_freq clock_sys_freq#define board_tim_update_step 1000000u#define board_tim_update_period 1000u在main()函数中调用了board_tim_init()函数,配置硬件定时器tim2产生1ms为周期的中断,并启动定时器。此时,对应在硬件定时器的中断服务程序中调用canopennode的定时器线程函数tmrtask_thread(),并在其中清硬件定时器中断的标志位。
/* timer thread executes in constant intervals ********************************/void tmrtask_thread(void){ increment_1ms(co_timer1ms); if (co- >canmodule[0]- >cannormal) { bool_t syncwas; /* process sync */ syncwas = co_process_sync(co, tmr_task_interval); /* read inputs */ co_process_rpdo(co, syncwas); /* further i/o or nonblocking application code may go here. */ /* write outputs */ co_process_tpdo(co, syncwas, tmr_task_interval); /* verify timer overflow */ if (tim_status_update_period == (tim_getinterruptstatus(board_tim_port) & tim_status_update_period) ) { co_errorreport(co- >em, co_em_isr_timer_overflow, co_emc_software_internal, 0u); tim_clearinterruptstatus(board_tim_port, tim_status_update_period); } }}/* timer interrupt function ***************************************************/void board_tim_irqhandler(void){ tim_clearinterruptstatus(board_tim_port, tim_status_update_period); tmrtask_thread();}这里要注意,canopennode原生的tmrtask_thread()函数的实现模板中,停用了“/* verify timer overflow */”之后的代码。这些被停用的代码,原本可以用来验证tmrtask_thread()函数内部操作,例如处理同步过程、读接收pdo和写发送pdo,在清了上一次1ms中断的硬件标志位后的1ms中是否能够执行完毕。如果tmrtask_thread()函数的处理时间过长,超出了一个周期任务的执行时间,此时检测到1ms定时器中断标志位再次置位,即出现超时。在1ms周期任务超时之后,canopen协议栈会认为这是一个可能产生风险的任务,因此可调用co_errorreport()函数将错误情况上报给canopen协议栈。
对接can驱动 co_driver.c & main.cco_driver.c文件中定义了大量的具体微控制器平台的can外设硬件模块相关的驱动函数,但在最基础的移植过程中,仅需重点关注4个函数即可:
co_canmode_init() - 初始化can外设模块,并配置好比特率、消息帧过滤器,以及收发中断等。co_cansend() - 将消息缓冲区中的数据搬运至can外设模块的硬件发送缓冲区中,即将发送can消息帧到can总线上。co_caninterrupt() - 绑定到can外设模块的硬件中断的服务程序,主要实现can硬件的接收过程,即将can外设模块从can总线上捕获下来的can消息帧数据转存到canopennode组件的接收缓冲区中,供协议栈进一步处理。当使用中断方式发送can消息帧时,也需要在co_caninterrupt()函数中调用co_cansend()函数发送can消息帧。co_canverifiyerrors() - 查看can外设模块的硬件错误。因为can总线上的消息帧需要经过仲裁才能上线,所以这里查错函数主要检查的是发送消息帧超时的情况。原生canopennode组件包中的co_driver.c文件中的函数已经实现了绝大部分同协议栈交互的业务逻辑,在具体微控制器平台上是适配时,仅需要将少量同硬件相关的步骤绑定到微控制器硬件的操作上即可。
co_canmodule_init()在co_driver.c文件中向co_canmodule_init()函数嵌入初始化flexcan外设模块的代码,包括初始化flexcan的通信引擎,配置好过滤器等(本移植工程未启硬件过滤器功能,由canopennode的软件过滤器处理)。
#include board_init.h/******************************************************************************/co_returnerror_t co_canmodule_init( co_canmodule_t *canmodule, void *candriverstate, co_canrx_t rxarray[], uint16_t rxsize, co_cantx_t txarray[], uint16_t txsize, uint16_t canbitrate){ uint16_t i; /* verify arguments */ if(canmodule==null || rxarray==null || txarray==null){ return co_error_illegal_argument; } /* configure object variables */ canmodule- >candriverstate = candriverstate; canmodule- >rxarray = rxarray; canmodule- >rxsize = rxsize; canmodule- >txarray = txarray; canmodule- >txsize = txsize; canmodule- >cannormal = false; canmodule- >usecanrxfilters = false;/* microcontroller dependent */ canmodule- >bufferinhibitflag = false; canmodule- >firstcantxmessage = true; canmodule- >cantxcount = 0u; canmodule- >errold = 0u; canmodule- >em = null; for(i=0u; i< rxsize; i++){ rxarray[i].ident = 0u; rxarray[i].mask = 0xffffu; rxarray[i].object = null; rxarray[i].pfunct = null; } for(i=0u; irtr) { mb.type = flexcan_mbtype_data; /* data frame type. */ } else { mb.type = flexcan_mbtype_remote; /* remote frame type. */ } mb.id = buffer- >ident; /* indicated id number. */ mb.format = flexcan_mbformat_standard; /* std frame format. */ mb.priority = board_flexcan_xfer_priority; /* the priority of the frame mb. */ /* set the information. */ mb.byte0 = buffer- >data[0]; mb.byte1 = buffer- >data[1]; mb.byte2 = buffer- >data[2]; mb.byte3 = buffer- >data[3]; mb.byte4 = buffer- >data[4]; mb.byte5 = buffer- >data[5]; mb.byte6 = buffer- >data[6]; mb.byte7 = buffer- >data[7]; /* set the workload size. */ mb.length = buffer- >dlc; /* send. */ bool status = flexcan_writetxmb(board_flexcan_port, board_flexcan_tx_mb_ch, &mb); flexcan_setmbcode(board_flexcan_port, board_flexcan_tx_mb_ch, flexcan_mbcode_txdataorremote); /* write code to send. */ return status;}co_returnerror_t co_cansend(co_canmodule_t *canmodule, co_cantx_t *buffer){ co_returnerror_t err = co_error_no; /* verify overflow */ if (buffer- >bufferfull) { if (!canmodule- >firstcantxmessage) { /* don't set error, if bootup message is still on buffers */ co_errorreport((co_em_t*)canmodule- >em, co_em_can_tx_overflow, co_emc_can_overrun, buffer- >ident); } err = co_error_tx_overflow; } co_lock_can_send(); if ( board_flexcan_txframe(buffer) ) /* copy the frame to can hardware. */ { canmodule- >bufferinhibitflag = buffer- >syncflag; } /* if no buffer is free, message will be sent by interrupt */ else { buffer- >bufferfull = true; canmodule- >cantxcount++; } co_unlock_can_send(); return err;}在嵌入co_cansend()函数时,有个要点:
这里在发送can消息帧实际实现了一个中断发送的机制。当在co_cansend()函数中调用brd_can_tx()函数时,程序将canopennode将要发送的消息帧数据搬运到can外设硬件的发送缓冲区中,并触发发送机制,等待在合适的时机将数据送上总线(需要等待获得仲裁才能将消息帧送上总线)。如果当前积压的发送消息数量为0,canmodule->cantxcount == 0,则可以向can外设的硬件发送缓冲区写数,否则,意味着当前can外设的硬件发送缓冲区中还有消息等待上线,此时只能记录一下计数器,canmodule->cantxcount++。后续的发送过程就需要在中断中的发送过程中完成了,在当前发送消息帧上线之后,发送完成,会触发can外设的中断服务程序,届时将检查canmodule->cantxcount计数器的值:如果已经是0,表示后续不需要再发帧了,那就清标志位,结束;如果不是0,那么在前一帧发送完成后,继续载入新的消息帧到硬件发送缓冲区,直到发送完消息队列中的最后一个消息,最后一次进中断,同上。canmodule->cantxcount计数器相当于是can硬件发送缓冲区的信号量,可以作为缓冲区是否为空的标志:若值为0,则对应硬件发送缓冲区为空;若值不为0,则用buffer->bufferfull标记can硬件发送缓冲区正在使用,同时使用canmodule->cantxcount计数器的值表示正在排队的数量。co_caninterrupt()在co_driver.c文件中编写brd_can_rx()函数,从can外设模块的硬件接收缓冲区中读收到的消息帧,然后嵌入在co_caninterrupt()函数中处理接收过程。
/******************************************************************************/void co_caninterrupt(co_canmodule_t *canmodule){ uint32_t status = flexcan_getmbstatus(board_flexcan_port); if (board_flexcan_rx_mb_status == (status & board_flexcan_rx_mb_status)) { /* receive interrupt */ co_canrxmsg_t *rcvmsg; /* pointer to received message in can module */ co_canrxmsg_t rcvmsgbuff; uint16_t index; /* index of received message */ uint32_t rcvmsgident; /* identifier of the received message */ co_canrx_t *buffer = null; /* receive message buffer from co_canmodule_t object. */ bool_t msgmatched = false; /* get message from module here */ rcvmsg = &rcvmsgbuff; flexcan_mb_type flexcan_rx_mb; flexcan_readrxmb(board_flexcan_port, board_flexcan_rx_mb_ch, &flexcan_rx_mb); rcvmsg- >ident = flexcan_rx_mb.id; rcvmsg- >dlc = flexcan_rx_mb.length; rcvmsg- >data[0] = flexcan_rx_mb.byte0; rcvmsg- >data[1] = flexcan_rx_mb.byte1; rcvmsg- >data[2] = flexcan_rx_mb.byte2; rcvmsg- >data[3] = flexcan_rx_mb.byte3; rcvmsg- >data[4] = flexcan_rx_mb.byte4; rcvmsg- >data[5] = flexcan_rx_mb.byte5; rcvmsg- >data[6] = flexcan_rx_mb.byte6; rcvmsg- >data[7] = flexcan_rx_mb.byte7; rcvmsgident = rcvmsg- >ident; /* can module filters are not used, message with any standard 11-bit identifier */ /* has been received. search rxarray form canmodule for the same can-id. */ buffer = &canmodule- >rxarray[0]; for (index = canmodule- >rxsize; index > 0u; index--) { if(((rcvmsgident ^ buffer- >ident) & buffer- >mask) == 0u) { msgmatched = true; break; } buffer++; } /* call specific function, which will process the message */ if (msgmatched && (buffer != null) && (buffer- >pfunct != null)) { buffer- >pfunct(buffer- >object, rcvmsg); } /* clear interrupt flag */ flexcan_clearmbstatus(board_flexcan_port, board_flexcan_rx_mb_status); } else if (board_flexcan_tx_mb_status == (status & board_flexcan_tx_mb_status)) { /* clear interrupt flag */ flexcan_clearmbstatus(board_flexcan_port, board_flexcan_tx_mb_status); /* first can message (bootup) was sent successfully */ canmodule- >firstcantxmessage = false; /* clear flag from previous message */ canmodule- >bufferinhibitflag = false; /* are there any new messages waiting to be send */ if (canmodule- >cantxcount > 0u) { uint16_t i; /* index of transmitting message */ /* first buffer */ co_cantx_t *buffer = &canmodule- >txarray[0]; /* search through whole array of pointers to transmit message buffers. */ for(i = canmodule- >txsize; i > 0u; i--) { /* if message buffer is full, send it. */ if (buffer- >bufferfull) { buffer- >bufferfull = false; canmodule- >cantxcount--; /* copy message to can buffer */ canmodule- >bufferinhibitflag = buffer- >syncflag; co_cansend(canmodule, buffer); break; /* exit for loop */ } buffer++; }/* end of for loop */ /* clear counter if no more messages */ if (i == 0u) { canmodule- >cantxcount = 0u; } } } else { /* some other interrupt reason */ }}在co_caninterrupt()函数中,实现了接收can消息帧和发送can消息帧的过程:
在接收过程中,can外设硬件收到总线上的消息帧后触发中断服务程序,程序从硬件接收缓冲区将消息帧读出来,填充到canmodule结构体的接收帧队列成员中,之后由成员对应的处理函数消化掉接收到的消息帧。在发送过程中,程序需要逐个处理掉之前的已经压入软件缓冲区中待发送的消息帧。当软件发送缓冲区为空时,由co_cansend()函数触发的发送过程会先把当前的消息帧写入硬件发送缓冲区中并启动发送,之后由发送完成事件触发中断。每次进入发送完成中断服务程序时,程序会先检查软件发送缓冲区中的消息帧的数量是不是0:如果是,说明后面没有需要继续发送的消息帧了,直接清标志位,收工;如果不是,说明还需要接着发送已经缓存的消息帧,那就再次调用co_cansend()函数搬运帧数据到硬件发送缓冲区中并触发的发送过程,之后由发送完成事件触发中断,直至最后清空发送缓冲区再清标志位。co_caninterrupt()函数将在main.c文件中被硬件的can中断服务函数调用。
/* can interrupt function *****************************************************///void /* interrupt */ co_can1interrupthandler(void){void brd_can_irqhandler(void){ co_caninterrupt(co- >canmodule[0]); /* clear interrupt flag */ /* the interrupt flags are cleared when processing each mb in flexcan. */ }co_canverifyerrors()在co_driver.c文件中co_caninterrupt()函数中,嵌入从can外设读错误计数值和状态标志位的代码,将硬件的错误状态反馈给canopennode协议栈。
void co_canverifyerrors(co_canmodule_t *canmodule){ uint16_t rxerrors, txerrors, overflow; co_em_t* em = (co_em_t*)canmodule- >em; uint32_t err; /* get error counters from module. id possible, function may use different way to * determine errors. */ //rxerrors = canmodule- >txsize; //txerrors = canmodule- >txsize; //overflow = canmodule- >txsize; rxerrors = (uint16_t) ((board_flexcan_port- >ecr & flexcan_ecr_rxerrcnt_mask) > > flexcan_ecr_rxerrcnt_shift); txerrors = (uint16_t) ((board_flexcan_port- >ecr & flexcan_ecr_txerrcnt_mask) > > flexcan_ecr_txerrcnt_shift); overflow = (uint16_t) ((board_flexcan_port- >esr1 & flexcan_esr1_errovr_mask) > > flexcan_esr1_errovr_shift); ...}这里的rxerrors和txerrors是can外设接收帧和发送帧的错误计数器,一般的can外设模块(例如flexcan),会对从can总线上捕获消息帧和发送消息帧进行超时管理,因为can总线的发送过程存在仲裁,确实可能在通信繁忙的时间段有一些优先级比较低(can id值比较大)的消息帧无法顺利发出。此时,如果有通信帧久久没有成功发出,则会上报给canopen协议栈,进一步可能会通过nmt协议调整网络通信的节奏,尽量让关键数据(由于延迟提升的优先级)得以通畅传输。
co_canclearpendingsyncpdos()另外,还需要在co_canclearpendingsyncpdos()函数中增加对flexcan硬件发送缓冲区的检查,当需要发送同步消息时,如果有未上线的消息占用发送消息缓冲区(当新的同步消息准备发出时,之前未发出的同步消息已经失效,不再具有同步的意义),则canopen可以强制腾空发送缓冲区,为最新的同步消息腾出空间准备发送。
/******************************************************************************/void co_canclearpendingsyncpdos(co_canmodule_t *canmodule){ uint32_t tpdodeleted = 0u; co_lock_can_send(); /* abort message from can module, if there is synchronous tpdo. * take special care with this functionality. */ if ( (board_flexcan_rx_mb_status == (board_flexcan_rx_mb_status & flexcan_getmbstatus(board_flexcan_port)) ) && canmodule- >bufferinhibitflag) { /* clear txreq */ canmodule- >bufferinhibitflag = false; tpdodeleted = 1u; } /* delete also pending synchronous tpdos in tx buffers */ if (canmodule- >cantxcount != 0u) { co_cantx_t *buffer = &canmodule- >txarray[0]; for (uint16_t i = canmodule- >txsize; i > 0u; i--) { if (buffer- >bufferfull) { if (buffer- >syncflag) { buffer- >bufferfull = false; canmodule- >cantxcount--; tpdodeleted = 2u; } } buffer++; } } co_unlock_can_send(); if (tpdodeleted != 0u) { co_errorreport((co_em_t*)canmodule- >em, co_em_tpdo_outside_window, co_emc_communication, tpdodeleted); }}至此,一个基本的使用canopennode组件实现的canopen的框架即移植完成。编译项目,清理可能的错误,即可下载工程的开发板运行程序。
build started: project: project*** using compiler 'v6.18', folder: 'c:\\keil_v5\\arm\\armclang\\bin'rebuild target 'target 1'compiling application.c......compiling co_trace.c...compiling crc16-ccitt.c...compiling eeprom.c...linking...program size: code=32600 ro-data=2468 rw-data=996 zi-data=6244 .\\objects\\project.axf - 0 error(s), 0 warning(s).build time elapsed: 00:00:02

人工智能上存在什么分歧
鸿蒙os基于什么开发 鸿蒙os属于什么系统
MAX4995A, MAX4995, MAX4995AL,
NB/Lora联网低功耗电池型物联网采集传感器
华为MateBook X详细对比苹果MacBook 谁更值得买?
基于FlexCAN适配CANopenNode
三星Galaxy S21系列One UI 5测试版在美国推出
谷歌为提高安卓手机电池续航时间,正在Android 11测试新功能
电视机的画质技术解读
虹软科技再获多家一线车厂前装项目 智能驾驶量产背后的实质与底牌
新品、实测、终端……MWC2023上海展,移远通信又为物联行业带来了哪些惊喜?
先进封装技术将成为突破半导体产业的关键
2019年DRAM芯片报价一路走低 1月涨价将是大概率事件
介绍一种MDB转换板 ,能够把MDB设备的付款金额转为脉冲输出
信部公布了2020年第14批共15家CDN牌照企业名单
对于新能源汽车该如何正确选择
步进电机及其驱动电路原理图
高通/联发科/海思等处理器厂商都有何特点?
数据通信中的模拟数据和数字数据
CH9141低功耗蓝牙串口透传芯片特点分析