// 本文部分内容来自网络

1. 启动过程

一般嵌入式处理器启动方式分为两种:
      1. XIP 模式  (eXecute In Place), 在该模式下CPU直接从Nor Flash上读代码执行,执行速度慢 ;
      2. 非XIP模式,  在该模式下硬件先将代码从Flash上搬移到RAM上后,CPU才能从RAM上访问数据,执行速度快;

对于非XIP模式,启动前需要先借助boot把代码段从FLASH拷贝到RAM;

对于XIP模式,如果FLASH基地址映射到0地址,复位后可以直接启动;

如果FLASH基地址非0地址,启动之前需要先借助boot在映射的0地址(笔者项目的使用SOC通过bootlink将0地址映射到了0x20110000 :IRAM_AON)中写入MSP的初始值+Reset入口地址(针对CortexM处理器)。

下文以CortexM + Zephyr1.9.2 +XIP模式 为例进行相关阐述。

__reset

根据向量表找到的reset入口,位于(zephyr\arch\arm\core\cortex_m\reset.S)

reset入口完成工作:

  • 初始化watchdog: _WdogInit
  • 将中断栈_interrupt_stack内容初始化为0xaa
  • 复位后CortexM处理器默认处于线程模式+特权访问+使用MSP主栈指针,这里切换为PSP指针并将PSP设为_interrupt_stack中断栈
  • 进入C语言准备_PrepC
SECTION_SUBSEC_FUNC(TEXT,_reset_section,__reset)/** The entry point is located at the __reset symbol, which* is fetched by a XIP image playing the role of a bootloader, which jumps to* it, not through the reset vector mechanism. Such bootloaders might want to* search for a __start symbol instead, so create that alias here.*/
SECTION_SUBSEC_FUNC(TEXT,_reset_section,__start)/* lock interrupts: will get unlocked when switch to main task */cpsid i#ifdef CONFIG_WDOG_INIT/* board-specific watchdog initialization is necessary */bl _WdogInit
#endif#ifdef CONFIG_INIT_STACKSldr r0, =_interrupt_stackldr r1, =0xaaldr r2, =CONFIG_ISR_STACK_SIZEbl memset
#endif/** Set PSP and use it to boot without using MSP, so that it* gets set to _interrupt_stack during nanoInit().*/ldr r0, =_interrupt_stackldr r1, =CONFIG_ISR_STACK_SIZEadds r0, r0, r1msr PSP, r0movs.n r0, #2   /* switch to using PSP (bit1 of CONTROL reg) */msr CONTROL, r0b _PrepC

_PrepC 

完成进入C语言环境后的准备工作,位于(zephyr\arch\arm\core\cortex_m\Prep_c.c)

_PrepC完成工作:

  • 重定位向量表
  • 使能浮点计算(如果有相关特性)
  • 清零BSS段
  • 从ROM拷贝数据段到RAM(针对XIP模式)
  • 正式进入C环境执行
void _PrepC(void)
{relocate_vector_table();enable_floating_point();_bss_zero();_data_copy();
#ifdef CONFIG_BOOT_TIME_MEASUREMENT__start_time_stamp = 0;
#endif_Cstart();CODE_UNREACHABLE;
}

relocate_vector_table

根据CortexM处理器的特性,向量表固定于0地址,对于(XIP模式并且FLASH基地址非0)或者(非XIP模式并且RAM基地址非0)的情况,需要重新将向量表拷贝到0地址。

#define VECTOR_ADDRESS 0static inline void relocate_vector_table(void)
{
#if defined(CONFIG_XIP) && (CONFIG_FLASH_BASE_ADDRESS != 0) || \!defined(CONFIG_XIP) && (CONFIG_SRAM_BASE_ADDRESS != 0)size_t vector_size = (size_t)_vector_end - (size_t)_vector_start;memcpy(VECTOR_ADDRESS, _vector_start, vector_size);
#endif
}

_vector_end/_vector_start两个符号的地址在linker.ld链接脚本里面有定义,linker文件位于(zephyr\include\arch\arm\cortex_m\scripts):

SECTION_PROLOGUE(_TEXT_SECTION_NAME,,){. = CONFIG_TEXT_SECTION_OFFSET;KEEP(*(.os.start))_vector_start = .;KEEP(*(.exc_vector_table))KEEP(*(".exc_vector_table.*"))KEEP(*(IRQ_VECTOR_TABLE))KEEP(*(.openocd_dbg))KEEP(*(".openocd_dbg.*"))/* Kinetis has to write 16 bytes at 0x400 */SKIP_TO_KINETIS_FLASH_CONFIGKEEP(*(.kinetis_flash_config))KEEP(*(".kinetis_flash_config.*"))#ifdef CONFIG_GEN_SW_ISR_TABLEKEEP(*(SW_ISR_TABLE))
#endif_vector_end = .;_image_text_start = .;*(.text)*(".text.*")*(.gnu.linkonce.t.*)} GROUP_LINK_IN(ROMABLE_REGION)_image_text_end = .;

_Cstart

正式进入C语言环境,开始操作系统初始化工作,位于(zephyr\kernel\init.c)

_Cstart完成工作:

  • 多线程初始化
  • PRE_KERNEL_1、PRE_KERNEL_2级别设备初始化
  • 切换到主线程开始运行
FUNC_NORETURN void _Cstart(void)
{
#ifdef CONFIG_TIME_TRACKinsert_pack_time_debug(PACK_AP_CSTART, NULL, -1);
#endif#ifdef CONFIG_ARCH_HAS_CUSTOM_SWAP_TO_MAINstruct k_thread *dummy_thread = NULL;
#elsestruct k_thread dummy_thread_memory;struct k_thread *dummy_thread = &dummy_thread_memory;
#endif/** Initialize kernel data structures. This step includes* initializing the interrupt subsystem, which must be performed* before the hardware initialization phase.*/prepare_multithreading(dummy_thread);/* perform basic hardware initialization */_sys_device_do_config_level(_SYS_INIT_LEVEL_PRE_KERNEL_1);_sys_device_do_config_level(_SYS_INIT_LEVEL_PRE_KERNEL_2);/* initialize stack canaries */
#ifdef CONFIG_STACK_CANARIES__stack_chk_guard = (void *)sys_rand32_get();
#endif/* display boot banner */switch_to_main_thread();/** Compiler can't tell that the above routines won't return and issues* a warning unless we explicitly tell it that control never gets this* far.*/CODE_UNREACHABLE;
}

prepare_multithreading

该函数完成多线程初始化,主要完成

  • 中断(NVIC)初始化
  • 创建了两个线程,分别是_main_thread(使用_main_stack,即系统入口对应的MSP指针)和_idle_thread(使用_idle_stack)
  • 初始化_timeout_q超时队列
  • 架构相关的内核初始化,针对CortexM主要完成MSP主栈设置为_interrupt_stack,以及中断异常相关初始化
static void prepare_multithreading(struct k_thread *dummy_thread)
{
#ifdef CONFIG_ARCH_HAS_CUSTOM_SWAP_TO_MAINARG_UNUSED(dummy_thread);
#else/** Initialize the current execution thread to permit a level of* debugging output if an exception should happen during kernel* initialization.  However, don't waste effort initializing the* fields of the dummy thread beyond those needed to identify it as a* dummy thread.*/_current = dummy_thread;dummy_thread->base.user_options = K_ESSENTIAL;dummy_thread->base.thread_state = _THREAD_DUMMY;
#endif/* _kernel.ready_q is all zeroes *//** The interrupt library needs to be initialized early since a series* of handlers are installed into the interrupt table to catch* spurious interrupts. This must be performed before other kernel* subsystems install bonafide handlers, or before hardware device* drivers are initialized.*/_IntLibInit();/* ready the init/main and idle threads */for (int ii = 0; ii < K_NUM_PRIORITIES; ii++) {sys_dlist_init(&_ready_q.q[ii]);}/** prime the cache with the main thread since:** - the cache can never be NULL* - the main thread will be the one to run first* - no other thread is initialized yet and thus their priority fields*   contain garbage, which would prevent the cache loading algorithm*   to work as intended*/_ready_q.cache = _main_thread;_new_thread(_main_thread, _main_stack,MAIN_STACK_SIZE, _main, NULL, NULL, NULL,CONFIG_MAIN_THREAD_PRIORITY, K_ESSENTIAL);_mark_thread_as_started(_main_thread);_add_thread_to_ready_q(_main_thread);#ifdef CONFIG_MULTITHREADING_new_thread(_idle_thread, _idle_stack,IDLE_STACK_SIZE, idle, NULL, NULL, NULL,K_LOWEST_THREAD_PRIO, K_ESSENTIAL);_mark_thread_as_started(_idle_thread);_add_thread_to_ready_q(_idle_thread);
#endifinitialize_timeouts();/* perform any architecture-specific initialization */kernel_arch_init();
}

_sys_device_do_config_level

设备初始化,Zephyr定义了四个级别的初始化,分别为:

#define _SYS_INIT_LEVEL_PRE_KERNEL_1 0
#define _SYS_INIT_LEVEL_PRE_KERNEL_2 1
#define _SYS_INIT_LEVEL_POST_KERNEL 2
#define _SYS_INIT_LEVEL_APPLICATION 3

_sys_device_do_config_level函数实现:

void _sys_device_do_config_level(int level)
{struct device *info;for (info = config_levels[level]; info < config_levels[level+1];info++) {struct device_config *device = info->config;device->init(info);
#ifdef CONFIG_TIME_TRACKinsert_pack_time_debug(ap_init_counter, device->init, level);ap_init_counter++;
#endif}
}

设备初始化接口定义:

SYS_INIT(netdog_work_q_init, APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT);#define SYS_INIT(init_fn, level, prio) \DEVICE_INIT(_SYS_NAME(init_fn), "", init_fn, NULL, NULL, level, prio)#define DEVICE_INIT(dev_name, drv_name, init_fn, data, cfg_info, level, prio) \DEVICE_AND_API_INIT(dev_name, drv_name, init_fn, data, cfg_info, \level, prio, NULL)#define DEVICE_AND_API_INIT(dev_name, drv_name, init_fn, data, cfg_info, \level, prio, api) \DEVICE_DEFINE(dev_name, drv_name, init_fn, \device_pm_control_nop, data, cfg_info, level, \prio, api)#define DEVICE_DEFINE(dev_name, drv_name, init_fn, pm_control_fn, \data, cfg_info, level, prio, api) \\static struct device_config _CONCAT(__config_, dev_name) __used \__attribute__((__section__(".devconfig.init"))) = { \.name = drv_name, .init = (init_fn), \.device_pm_control = (pm_control_fn), \.config_info = (cfg_info) \}; \static struct device _CONCAT(__device_, dev_name) __used \__attribute__((__section__(".init_" #level STRINGIFY(prio)))) = { \.config = &_CONCAT(__config_, dev_name), \.driver_api = api, \.driver_data = data \}

switch_to_main_thread

切换到_main_thread开始运行:

static void switch_to_main_thread(void)
{
#ifdef CONFIG_ARCH_HAS_CUSTOM_SWAP_TO_MAIN_arch_switch_to_main_thread(_main_thread, _main_stack, MAIN_STACK_SIZE,_main);
#else/** Context switch to main task (entry function is _main()): the* current fake thread is not on a wait queue or ready queue, so it* will never be rescheduled in.*/_Swap(irq_lock());
#endif
}

_main_thread线程入口为_main,_main函数完成:

  • 平台相关的初始化以及_SYS_INIT_LEVEL_POST_KERNEL、SYS_INIT_LEVEL_APPLICATION级别设备初始化,这些初始化会创建相关的平台与应用线程;
  • 完成后进入main钩子函数(\os\zephyr\samples\xxx\src\main.c),本平台该函数实现为空;
  • 该线程没有死循环入口,所以完成后自行退出,然后操作系统开始正式调度执行;
static void _main(void *unused1, void *unused2, void *unused3)
{ARG_UNUSED(unused1);ARG_UNUSED(unused2);ARG_UNUSED(unused3);oss_event_init();oss_icp_init();oss_nv_init();_reset_uart_termios();ramdump_init();amt_init();zcat_ap_init();_sys_device_do_config_level(_SYS_INIT_LEVEL_POST_KERNEL);/* Final init level before app starts */_sys_device_do_config_level(_SYS_INIT_LEVEL_APPLICATION);#if defined(CONFIG_BOOT_DELAY) && CONFIG_BOOT_DELAY > 0if (boot_delay > 0) {printk("delaying boot\n");k_sleep(CONFIG_BOOT_DELAY);}PRINT_BOOT_BANNER();
#endif_init_static_threads();#ifdef CONFIG_BOOT_TIME_MEASUREMENT/* record timestamp for kernel's _main() function */extern u64_t __main_time_stamp;__main_time_stamp = (u64_t)k_cycle_get_32();
#endifextern void main(void);k_thread_priority_set(_main_thread, CONFIG_MAIN_THREAD_PRIORITY);main();/* Terminate thread normally since it has no more work to do */_main_thread->base.user_options &= ~K_ESSENTIAL;
}

 

2. 中断响应

以CortexM0处理器为例,其异常列表如下:

异常类型      异常编号        描述Reset          1         上电复位或系统复位NMI           2         不可屏蔽中断Hard fault      3         用于错误处理,系统检测到错误后被激活SVCall         11         请求管理调用,在执行SVC指令被激活,主要用作操作系统PendSV         14         可挂起服务(系统)调用SysTick        15         系统节拍定时器异常,一般在OS种用作周期系统节拍异常IRQ0-IRQ31       16-47        中断,可来自于外部,也可来自片上外设

查看linker.ld文件可以看到,Zephyr系统将上述异常对应的向量表分成了几部分,分别为exc_vector_tableIRQ_VECTOR_TABLESW_ISR_TABLE;(忽略openocd_dbg、kinetis_flash_config段,未使用):

        _vector_start = .;KEEP(*(.exc_vector_table))KEEP(*(".exc_vector_table.*"))KEEP(*(IRQ_VECTOR_TABLE))KEEP(*(.openocd_dbg))KEEP(*(".openocd_dbg.*"))/* Kinetis has to write 16 bytes at 0x400 */SKIP_TO_KINETIS_FLASH_CONFIGKEEP(*(.kinetis_flash_config))KEEP(*(".kinetis_flash_config.*"))#ifdef CONFIG_GEN_SW_ISR_TABLEKEEP(*(SW_ISR_TABLE))
#endif_vector_end = .;

第一部分,异常向量表exc_vector_table,位于(zephyr\arch\arm\core\cortex_m\vector_table.S),定义了1-15号异常对应的跳转PC地址:

 SECTION_SUBSEC_FUNC(exc_vector_table,_vector_table_section,_vector_table).word _main_stack + CONFIG_MAIN_STACK_SIZE.word __reset.word __nmi.word __hard_fault.word __reserved.word __reserved.word __reserved.word __reserved.word __reserved.word __reserved.word __reserved.word __svc.word __reserved.word __reserved.word __pendsv#if defined(CONFIG_CORTEX_M_SYSTICK).word _timer_int_handler#else.word __reserved#endif

第二部分,中断向量表IRQ_VECTOR_TABLE,定义了16-47号中断跳转PC地址,位于project\prj_XXX\temp\Isr_tables.c,这是一个正式编译前由gen_isr_tables.py Python脚本生成的文件;该向量表所有项都对应同一个函数即所有中断总入口,函数名为_isr_wrapper

#define __irq_vector_table _GENERIC_SECTION(IRQ_VECTOR_TABLE)u32_t __irq_vector_table _irq_vector_table[34] = {0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,0xffc88cd,
};

第三部分,具体中断实现结构体SW_ISR_TABLE,同样位于project\prj_XXX\temp\Isr_tables.c,也是由Python脚本生成,该结构体由两项成员,分别为传入参数arg和中断入口isr

#define __sw_isr_table  _GENERIC_SECTION(SW_ISR_TABLE)struct _isr_table_entry {  void *arg;  void (*isr)(void *); };struct _isr_table_entry __sw_isr_table _sw_isr_table[34] = {{(void *)0x10724, (void *)0xffb8479},{(void *)0x10670, (void *)0xffb4b59},{(void *)0x0, (void *)0xffc8759},{(void *)0x1067c, (void *)0xffb4b59},{(void *)0x0, (void *)0xffc8759},{(void *)0x0, (void *)0xffb9661},{(void *)0x0, (void *)0xffc8759},{(void *)0x0, (void *)0xffc8759},{(void *)0x10730, (void *)0xffb8aa9},{(void *)0x10730, (void *)0xffb8a5d},{(void *)0x106c4, (void *)0xffb52c9},{(void *)0x2, (void *)0xffc6ad1},{(void *)0x0, (void *)0xffc6ad1},{(void *)0x1061c, (void *)0xffb66f5},{(void *)0x10610, (void *)0xffb66f5},{(void *)0x0, (void *)0xffc8759},{(void *)0x0, (void *)0xffc8759},{(void *)0x0, (void *)0xffc69e9},{(void *)0x0, (void *)0xffc8759},{(void *)0x10694, (void *)0xffb9b8b},{(void *)0x0, (void *)0xffc72e1},{(void *)0x106e8, (void *)0xffb6af3},{(void *)0x0, (void *)0xffc8759},{(void *)0x0, (void *)0xffc8759},{(void *)0x0, (void *)0xffc8759},{(void *)0x0, (void *)0xffc8759},{(void *)0x0, (void *)0xffc8759},{(void *)0x0, (void *)0xffc8759},{(void *)0x10730, (void *)0xffb8959},{(void *)0x0, (void *)0xffc8759},{(void *)0x106b8, (void *)0xffb52c9},{(void *)0x0, (void *)0xffc5e9d},{(void *)0x0, (void *)0xffc8759},{(void *)0x0, (void *)0xffc8759},
};

具体程序如何从_isr_wrapper总入口找到对应中断并进入对应中断入口,可以参考_isr_wrapper函数实现:

SECTION_FUNC(TEXT, _isr_wrapper)push {lr}     /* lr is now the first item on the stack *//** All interrupts are disabled when handling idle wakeup.  For tickless* idle, this ensures that the calculation and programming of the device* for the next timer deadline is not interrupted.  For non-tickless idle,* this ensures that the clearing of the kernel idle state is not* interrupted.  In each case, _sys_power_save_idle_exit is called with* interrupts disabled.*/cpsid i  /* PRIMASK = 1 *//* is this a wakeup from idle ? */ldr r2, =_kernel/* requested idle duration, in ticks */ldr r0, [r2, #_kernel_offset_to_idle]cmp r0, #0beq _idle_state_clearedmovs.n r1, #0/* clear kernel idle state */str r1, [r2, #_kernel_offset_to_idle]blx _sys_power_save_idle_exit
_idle_state_cleared:cpsie i     /* re-enable interrupts (PRIMASK = 0) */mrs r0, IPSR   /* get exception number */ldr r1, =16subs r0, r1   /* get IRQ number */lsls r0, #3 /* table is 8-byte wide */ldr r1, =_sw_isr_tableadd r1, r1, r0 /* table entry: ISRs must have their MSB set to stay* in thumb mode */ldm r1!,{r0,r3}   /* arg in r0, ISR in r3 */blx r3        /* call ISR */pop {r3}mov lr, r3/* exception return is done in _IntExit() */b _IntExit

中断定义与处理

中断定义接口如下:

 IRQ_CONNECT(SI_TIM0_IRQ, CONFIG_TIMER_0_IRQ_PRI,timer_si_isr, DEVICE_GET(timer_si_0), 0);

中断号:SI_TIM0_IRQ

中断处理函数:timer_si_isr

定义完成后通过脚本解析,timer_si_isr的地址会被写入sw_isr_table;下面的中断处理路径,通过timer中断实现了系统tick的处理:

timer_si_isr
timer_systick_callback
_sys_clock_final_tick_announce
_sys_clock_tick_announce
_nano_sys_clock_tick_announce
handle_timeouts
_handle_expired_timeouts
_handle_one_expired_timeout
work_timeout

转载于:https://www.cnblogs.com/DF11G/p/9774437.html

Zephyr启动过程与中断响应相关推荐

  1. u-boot启动过程

    目录: 一.初识u-boot 3 1,Bootloader介绍 3 2,Bootloader的启动方式 3 (1)网络启动方式 4 (2)磁盘启动方式 4 (3)Flash启动方式 4 3,U-boo ...

  2. U-Boot启动过程--详细版的完全分析

    目录: 一.初识u-boot 3 1,Bootloader介绍 3 2,Bootloader的启动方式 3 (1)网络启动方式 4 (2)磁盘启动方式 4 (3)Flash启动方式 4 3,U-boo ...

  3. 【ARM-Linux开发】U-Boot启动过程--详细版的完全分析

    ---------------------------------------------------------------------------------------------------- ...

  4. (转)U-Boot启动过程--详细版的完全分析

    原文出处:http://blog.csdn.net/sydjm/article/details/8591518 -------------------------------------------- ...

  5. 【转】U-Boot启动过程--详细版的完全分析

    我们知道,bootloader是系统上电后最初加载运行的代码.它提供了处理器上电复位后最开始需要执行的初始化代码. 在PC机上引导程序一般由BIOS开始执行,然后读取硬盘中位于MBR(Main Boo ...

  6. Android系统的启动过程

    Android系统的启动过程可以简单地总结为以下几个流程: 加载BootLoader -> 初始化内核 -> 启动init进程 -> init进程fork出Zygote(孵化器)进程 ...

  7. Android系统默认Home应用程序(Launcher)的启动过程源代码分析

    在前面一篇文章中,我们分析了Android系统在启动时安装应用程序的过程,这些应用程序安装好之后,还需要有一个Home应用程序来负责把它们在桌面上展示出来,在Android系统中,这个默认的Home应 ...

  8. Linux0.11内核引导启动过程概述

    Linux0.11仅支持x86架构.它的内核引导启动程序在文件夹boot内,共有三个汇编代码文件.按照启动流程依次是: (1)bootsect.s.boot是启动引导的意思,sect即sector,是 ...

  9. linux启动sql server数据库,SQL Server数据库启动过程详解及启动不起来的问题分析及解决方法...

    第五步.启动系统数据库model model系统数据库同样也是SQL Server启动过程中用到的一个非常关键的数据库,如果这个库损坏,SQL Server启动也会失败,关于model数据不能启动的原 ...

最新文章

  1. 没有添加跳新增,添加之后跳修改
  2. 文本查找查找命令的grep 、egrep、fgrep用法的详解
  3. 13. PDE_PTE属性
  4. Arraylist、HashSet去重复 treeSet排列实现方法 HashMap遍历取值
  5. uva 1587(Box UVA - 1587)
  6. 牛逼的程序员,都长什么样?
  7. 陕西省计算机二级报名流程,计算机二级考试报名流程
  8. You can‘t specify target table ‘XXX‘ for update in FROM clause
  9. 阿里云应用高可用服务公测发布
  10. 推荐两款工具给爱做实验的人
  11. android apk 防止反编译技术加壳技术(转)
  12. eclipse3.7.2+KEmulator搭建J2ME开发环境
  13. 关于连接PostgreSQL时提示 FATAL: password authentication failed for user 连接用户名 的解决方法...
  14. 关于node.js,dataStr的undefined的问题
  15. easyconnect(mac版)总是初始化问题
  16. 7.Flink实时项目之独立访客开发
  17. win10重装系统后连不上公司服务器,电脑重装win10系统连不上网,怎么办?
  18. 科研伦理与学术规范课后答案
  19. android美颜sdk,Android美颜SDK能否占据现有的市场
  20. 清华计算机408考研真题资料经验分享

热门文章

  1. 宝可梦探险寻宝料理php,宝可梦探险寻宝料理配方 宝可梦探险寻宝食谱一览
  2. 程序员如何用技术变现?(取其精华去其糟粕)
  3. Skyline产品总体介绍
  4. 实验4-2-6 输出三角形字符阵列 (15分)本题要求编写程序,输出n行由大写字母A开始构成的三角形字符阵列。
  5. 关于多部门协作完成项目使用过程中出现问题互相推卸责任的问题
  6. Facebook、微软、腾讯、DiDi message等全球科技公司都在抢先布局元宇宙
  7. Cmaker 是什么
  8. python 傅立叶函数_python-如何从图像中去除高频内容以进行傅立叶逆变换
  9. 阿里云智能总裁张建锋:“全面上云的拐点到了!”
  10. 【电源专题】SMPS电源的EMI来源和测量方法