采用DS80C400芯片软件的互联网扬声器

描述

DS80C400微处理器的网络功能使其成为设计简单以太网扬声器的自然选择。通过使用处理器ROM内置的TCP/IP堆栈,用8051汇编编写的应用程序可以轻松地从网络读取流音频数据,并使用该数据驱动数模转换器(DAC),为一组扬声器提供线路电平输出。本应用笔记介绍了运行支持以太网的简单扬声器所需的硬件设计和软件。

系统概述

软件

在顶层,应用程序由一台主机组成,通过网络连接将未压缩的音频(如WAV文件中的数据)发送到DS80C400,DS1C<>监听和播放音频数据。图<>显示了该系统的框

扬声器


图1.系统框图。

必须有两个软件应用程序才能使该系统工作。一个应用程序必须在主机上运行,并将音频数据发送到DS80C400。另一个应用程序必须在DS80C400上运行并播放音频数据。

主机应用程序在此系统中的工作很容易。它必须从 WAV 文件中读取原始音频数据并通过网络发送。由于主机上没有使用大量处理能力,因此它也执行其他一些工作,例如流控制和简单的数据格式化。

DS80C400上的应用稍微复杂一些。它需要通过网络接收音频数据,并以指定的采样率将该数据推送到音频电路。

接收音频数据是在循环中实现的,该循环等待音频数据,并在音频数据可用时将其写入循环缓冲区。当它接收新数据时,它还必须维护一个指向缓冲区中有效数据末尾的指针,以便应用程序不会播放无效数据。

扬声器应用的第二部分是将数据推送到音频电路的部分。音频数据被馈送到数模转换器,该转换器反过来驱动普通的计算机扬声器。由于常规时序对音频应用至关重要,因此应用的这一部分作为定时器中断实现。图 2 显示了应用程序的循环和计时器部分如何通过循环音频缓冲区进行交互。

扬声器


图2.循环音频缓冲区。

硬件

图3所示为音频电路示意图,可连接至TINIm400验证模块或基于DS80C400的定制设计。对于这个项目,扬声器应用程序是在最初为网络摄像机设计的电路板上开发的,并进行了一些小的修改。

扬声器


图3.硬件框图。

在此配置中,数模转换器提供 0 至 2V 的输出。由于线路电平扬声器输入为±1V,因此扬声器的接地连接到1V。本电路中使用的数模转换器是MAX542,精度为16位。串行数据可以通过DS80C400的串行端口传递到DAC,这比以编程方式切换时钟和数据引脚要快得多。MAX542具有一条片选线,在串行负载期间必须保持低电平,负载信号(Load DAC)必须在所有串行数据写入后保持低电平。

主机应用程序:发送未压缩的音频

主机应用程序是一个名为 SendDataTCP 的 Java™ 类。它是一个Java应用程序,读取PCM编码的WAV文件,执行一些简单的格式化,并通过TCP连接将音频样本块发送到DS80C400。

该程序假设正在读取的WAV文件包含立体声,16位数据,以44.1kHz的采样率播放。但是,该应用程序支持发送 44.1、22.05 和 11.025kHz 的采样率,因此音频数据可能需要重新格式化。假设WAV文件中的数据为16位立体声,因此每个样本由4个字节组成(通道2为1字节,通道2为2字节)。如果DS80C400需要单声道数据而不是立体声数据,则仅从WAV文件中提取一个通道。如果采样率低于44.1kHz,则跳过某些样本。例如,如果DS80C400需要采样率为22.05kHz的立体声数据,则SendDataTCP程序将发送2字节的通道1数据,发送2字节的通道2数据,然后跳过下一个样本。如果预计单声道数据频率为22.05kHz,则SendDataTCP程序将发送2字节的通道1数据,跳过通道2部分,然后跳过整个下一个样本。

在发送数据之前,必须再执行两次转换。首先,必须将示例从有符号数据转换为无符号数据。WAV文件包含表示-1至1之间电压的有符号数据,但MAX542接受表示0至2之间电压的无符号数据。请注意,由于电路为扬声器提供1V的虚拟地,因此所需的转换是简单地在WAV文件中定义的电压上增加1 V。由于输入值8000十六进制代表MAX1输出的542V,因此需要为每个8000位采样增加16十六进制。请注意,这与切换样本的高位的操作相同。表 1 显示了来自 WAV 文件的单个 16 位样本、所需电压、未改变样本产生的电压以及 改变后的样本将产生的电压。

表 1.改变音频样本以实现所需的电压输出

 

16 位音频样本(十六进制) 所需电压 来自未改变样品的电压 更改的样本 来自改变样品的电压
0000 1.00 0.00 8000 1.00
7FFF 2.00 1.00 FFFF 2.00
8000 0.00 1.00 0000 0.00
4000 1.50 0.50 C000 1.50
C000 0.50 1.50 4000 0.50

 

必须发生的第二个转换是位翻转操作。DS80C400上的串行端口首先写入最低有效位,但MAX542期望数据最高有效位优先。此操作是使用简单的查找表执行的。

数据以80字节块的形式发送到DS400C1400 - 该尺寸可提供最佳性能。数据流控制是通过跟踪上一秒内发送的数据量并将其与每秒预期发送的数据量进行比较来执行的。例如,采样率为22.05kHz的单声道数据每秒将产生44,100字节。如果 SendDataTCP 程序在过去 44 毫秒内发送了 500,800 字节,它将休眠大约 200 毫秒。DS80C400使用超过400kB的缓冲器,相当于几秒钟的音频数据。因此,准确的计时在SendDataTCP程序中很重要,但并不重要。一些变化是可以接受的。

请注意,SendDataTCP程序通常会尽可能快地发送数据。如果程序由于在最后一秒发送了太多数据而从未暂停,则可能是数据速率过高,应用程序无法处理。这可能是网络流量过多的结果。

DS80C400:初始化扬声器应用

DS80C400的应用完全用8051汇编编写。请注意,也可以使用 Keil 的编译器在 C 中实现应用程序,或者使用 TINI® 运行时环境在 Java 中实现应用程序。该应用程序足够小,因此在汇编中编写它并不是一项艰巨的任务。

在可能的情况下,扬声器应用程序利用了ROM中的功能未占用或更改的资源。DS80C400有4个数据指针,其中只有一个不作系统更改。前两个数据指针被所有函数广泛使用,尤其是对于复制操作。第四个数据指针在某些网络例程中使用,但始终保留。从不使用第三个数据指针。由于驱动扬声器的中断需要是高优先级中断,因此第四个数据指针不适合使用,只剩下第三个数据指针可用。DS80C400还具有四个定时器。ROM 使用定时器 0 作为时钟周期,使用定时器 2 作为串行端口输出。这将计时器 1 和计时器 3 留给扬声器应用程序。

扬声器应用使用定时器3产生中断,用于加载MAX542数模转换器。选择定时器 3 以在 16 位定时器模式下运行。在 3 位模式下,计时器 16 没有自动重新加载,尽管硬件会自动清除中断位。定时器3中断作为高优先级运行,因为加载MAX542的时序对音频质量至关重要。

在应用启动之前,ROM已经设置了DS80C400的一些特殊功能。该处理器已经处于 24 位寻址模式,允许跨 64kB 边界轻松访问代码和数据。扩展堆栈也已启用,利用DS80C400的专用1024字节堆栈空间。这留下了间接内存空间可供应用程序使用,而不必担心堆栈使用会破坏其内容。应用启动后,时钟四倍器使能,产生约54ns的单周期指令时间。接下来,初始化定时器3,这必须在ROM初始化和进程交换开始之前完成。这是因为ROM在进程交换时保留了中断启用位。由于计时器中断需要一直运行,因此应在启用进程本身之前启用它。

为了完成系统的初始化,调用了许多ROM函数。调用的第一个 ROM 函数是 rom_init,它初始化内存管理器、进程管理器和网络堆栈。接下来设置网络参数,为DS80C400提供静态IP地址。

系统现已初始化并准备好创建侦听套接字。网络功能是传统伯克利式套接字的组装版本。应用程序通过调用create_socket创建新的 TCP 套接字句柄,并通过调用bind_socket将其分配给端口号。该函数setup_listen将套接字设置为服务器套接字,accept_connection等待套接字连接。

在程序进入主循环之前,读取和写入指针被初始化。传入数据来自 网络连接将写入存储在间接内存区域中的 EndBuffer 指针,因为没有可免费使用且跨进程交换安全的直接。第三个数据指针用于从缓冲区读取下一个有效样本。此指针由计时器 3 的中断服务例程 (ISR) 独占使用。在 ISR 读取示例数据之前,它会检查它是否也在读取 靠近 EndBuffer 指针。如果两个指针位于同一组(相同的 64kB 内存区域)中,则计时器 ISR 将简单地退出而不播放音频数据。这不仅可以防止 ISR 读取缓冲区末尾以外的无效数据,但也提供一定量的缓冲,以防应用程序接收数据的速度不够快。如果应用程序停止播放音频数据,则在至少有 64,000 字节可用之前,它不会再次启动。这里的权衡是,如果应用程序接收数据的速度不够快,则可以听到音频中较长的间隙,但音频是可识别的。

循环:等待来自网络的数据

在应用程序的主循环开始等待数据之前,它会检查 EndBuffer 指针以查看它是否已环绕在循环缓冲区的末尾,并在必要时调整指向循环缓冲区开头的指针。然后,它调用 recv_data 函数,该函数读取任何可用数据或块,直到数据可用。接收到的网络数据直接读入循环缓冲区。这可以防止应用程序在recv_data函数返回后复制数据。如果 EndBuffer 指针靠近循环缓冲区的末尾,则 recv_data 函数仅请求足够的数据到达缓冲区的末尾。这意味着应用程序有时可能会请求接收少量数据,但好处是应用程序可以直接将数据读取到循环缓冲区中,而无需中间副本。读取后,将更新 EndBuffer 指针,控件返回到循环的顶部。

如果在读取时发生错误,应用程序将关闭其套接字并等待另一个套接字连接。通常,检测到的错误实际上意味着主机关闭了发送套接字。这允许发送方随时启动和停止主机程序,并依次播放多个WAV文件。

计时器中断:播放音频数据

在执行任何任务之前,计时器 3 的中断服务例程 (ISR) 必须重新加载计时器寄存器。计时器寄存器始终以相同的值重新加载。此重新加载值与音频样本的播放速率相关联。较高的重新加载值(意味着计数器翻转的时间越短)意味着更快的音频样本播放速度。较低的重新加载值意味着音频样本播放速度较慢。

重新加载计时器寄存器后,ISR 会检查它读取的数据是否太靠近 EndBuffer 指针。仅检查银行编号(指针的最高字节)有两个好处。在前面初始化扬声器应用程序一节中已经讨论了一个 - 当应用程序接收数据的速度不够快时,防止出现短而难以理解的音频突发。另一个好处是可以更快地比较 ISR。ISR 每秒运行数千次,因此从 ISR 切割周期非常重要。通过仅检查高地址字节,可以避免对中间和低地址字节进行两次额外的比较。

如果有可供播放的有效音频数据,则会读取样本,并将数据指针递增到下一个样本。数据加载到MAX542数模转换器,首先将片选线设置为低电平,将2个字节加载到串行端口,将片选线设置为高电平,然后将负载DAC线脉冲至低电平。串行端口处理串行时钟和数据线的正确切换。每次加载串行端口后都会插入几个nop指令,允许硬件完成字节移出。最后,ISR 检查读取音频数据的指针,以查看它是否已环绕在循环缓冲区的末尾,并在必要时进行更正。

滴答声:覆盖系统计时器

为了以允许高质量音频播放的速率接收数据,需要更改操作系统的计时器滴答功能。更改计时器时钟周期将允许对 I/O 性能进行更多控制。以下是DS80C400 ROM中运行时的原始定时器滴答代码:

 

   IOPOLL_TICK_MS    equ    4

      WOS_Tick:
             ; The timer is running in divide by 12 mode.
             push    psw
             push    acc

             clr    tr0
             clr    tf0
             mov    a, sched_reload_lsb
             add    a, tl0
             mov    tl0, a
             mov    a, sched_reload_msb
             addc   a, th0
             mov    th0, a
             setb   tr0

             inc    ms_count_0
             mov    a, ms_count_0
             jnz    wos_tick_check_sched        ; Check for byte 0 roll.
             inc    ms_count_1
             mov    a, ms_count_1
             jnz    wos_tick_check_sched        ; Check for byte 1 roll.
             inc    ms_count_2
             mov    a, ms_count_2
             jnz    wos_tick_check_sched        ; Check for byte 2 roll.
             inc    ms_count_3
             mov    a, ms_count_3
             jnz    wos_tick_check_sched        ; Check for byte 3 roll.
             inc    ms_count_4                  ; If this wraps, we are in trouble

      wos_tick_check_sched:
             jb     need_sched, wos_tick_check_critical_section

             mov    a, ms_count_0               ; See if it's time to run the
             anl    a, #IOPOLL_TICK_MS-1        ;    scheduler/iopoll routines.
             jnz    wos_timer_reload            ; If not, don't do scheduler stuff.

      wos_tick_check_critical_section:
             clr    ea                          ; Make sure nobody interrupts
                                                ; us before we want to

             mov    a, STATUS                   ; Check for low priority interrupts
             jb     acc.5, wos_tick_low_priority_in_progress
                                                ; If low priority interrupts are being
                                                ; serviced, don't run the scheduler.
                                                ; If we don't do this, we'll start running
                                                ; the scheduler as a low priority interrupt.

             mov    a, wos_crit_count           ; Check the critical section count.
             jz     wos_tick_not_critical_section
                                                ; If we're not in a critical section,
                                                ; go ahead, jump and run the scheduler.

      wos_tick_low_priority_in_progress:
             setb   need_sched                  ; Signal to ourselves, or whoever, that
                                                ; we need to run the scheduler next time
             sjmp   wos_timer_reload            ; Going to blow off this tick.

      wos_tick_not_critical_section:
             WOS_ENTER_CRITICAL_SECTION
             pop    acc                         ; Clean up stack.
             pop    psw
             pop    curr_pc_x                   ; Return address to get out of interrupt.
             pop    curr_pc_h
             pop    curr_pc_l
             PUSH_DPTR1
             push   dps
             mov    dps, #0
             mov    dptr, #WOS_IOPoll           ; Get address of IOPoll
             mov    sched_l, dpl
             mov    sched_h, dph
             mov    sched_x, dpx
             pop    dps
             POP_DPTR1

             push   sched_l                     ; Push address of IOPoll
             push   sched_h
             push   sched_x
             reti                               ; Run IOPoll

      wos_timer_reload:
             ; Interrupts must have been on when the interrupt handler
             ; was called.
             setb   ea                          ; Enable interrupts
             pop    acc
             pop    psw
             reti
 
 

的计算结果始终为 0,从而允许一些代码和逻辑减少。此外,由于我们不担心准确的系统时钟计时,因此我们可以减少时钟重新加载代码。

一个复杂情况是通过覆盖计时器滴答函数来引入的。扬声器代码应在DS80C400 ROM的任何未来版本上运行,因此我们无法对WOS_IOPoll功能的地址进行硬编码。幸运的是,WOS_IOPoll函数的地址在 ROM 导出表中。扬声器程序在启动时读取此地址并将其存储在间接内存中,然后由计时器 tick 函数用于调用 WOS_IOPoll 函数。以下是针对扬声器应用定制的定时器滴答功能:

 

   speaker_wos_tick:
             ; The timer is running in divide by 12 mode.
             push   psw
             push   acc

             ;
             ; We know what we want our timer reload to be.
             ; And our millisecond count doesn't have to be too
             ; accurate, so we can just straight load the
             ; timer registers.
             ;
             mov    tl0, #TICK_RELOAD_LOW       ; TICK_RELOAD_LOW  = 80h
             mov    th0, #TICK_RELOAD_HIGH      ; TICK_RELOAD_HIGH = FDh

             inc    ms_count_0
             mov    a, ms_count_0
             jnz    wos_tick_check_sched        ; Check for byte 0 roll.
             inc    ms_count_1
             mov    a, ms_count_1
             jnz    wos_tick_check_sched        ; Check for byte 1 roll.
             inc    ms_count_2
             mov    a, ms_count_2
             jnz    wos_tick_check_sched        ; Check for byte 2 roll.
             inc    ms_count_3
             mov    a, ms_count_3
             jnz    wos_tick_check_sched        ; Check for byte 3 roll.
             inc    ms_count_4                  ; If this wraps, we are in trouble

      wos_tick_check_sched:
             clr    ea                          ; Make sure nobody interrupts
                                                ; us before we want to
             mov    a, STATUS                   ; Check for low priority interrupts
             jb     acc.5, wos_tick_low_priority_in_progress
                                                ; If low priority interrupts are being
                                                ; serviced, don't run the scheduler.
                                                ; If we don't do this, we'll start running
                                                ; the scheduler as a low priority interrupt.
             mov    a, wos_crit_count           ; Check the critical section count.
             jz     wos_tick_not_critical_section
                                                ; If we're not in a critical section, go
                                                ; ahead, jump and run the scheduler.

      wos_tick_low_priority_in_progress:
             setb   need_sched                  ; Signal to ourselves, or whoever, that we
                                                ; need to run the scheduler next time
             sjmp   wos_timer_reload            ; Going to blow off this tick.

      wos_tick_not_critical_section:
             WOS_ENTER_CRITICAL_SECTION
             mov    psw, #0
             push   r0_b0
             mov    r0, #wos_iopoll_x
             mov    a, @r0                      ; xhigh byte of wos_iopoll address
             inc    r0
             mov    sched_x, a
             mov    a, @r0                      ; high byte of wos_iopoll address
             inc    r0
             mov    sched_h, a
             mov    a, @r0                      ; low byte of wos_iopoll address
             inc    r0
             mov    sched_l, a
             pop    r0_b0

             pop    acc                         ; Clean up stack.
             pop    psw

             pop    curr_pc_x                   ; Return address to get out of interrupt.
             pop    curr_pc_h
             pop    curr_pc_l

             push   sched_l                     ; Push address of IOPoll
             push   sched_h
             push   sched_x

             reti                               ; Run IOPoll

      wos_timer_reload:
             ; Interrupts must have been on when the interrupt handler
             ; was called.
             setb   ea                          ; Enable interrupts
             pop    acc
             pop    psw
             reti
 

应用:构建和运行

主机应用程序是 Java 应用程序,因此需要 Java 开发工具包来构建和运行它。在开发过程中使用了版本 1.3.1,但 SendDataTCP 程序中的代码非常简单,任何已发布的 Java 开发工具包版本都应该足够了。要构建的命令行很简单:

      javac SendDataTCP.java
 

若要运行 SendDataTCP 程序,请使用如下所示的命令行: 在本例中,10.0.0.1 是侦听端口 80 上的连接的 DS400C5555 的 IP 地址。WAV文件some_song.wav将用于将音频样本发送到DS80C400。请注意,假定使用的WAV文件包含44.1kHz立体声数据样本。有几种工具可用于从 MP3 文件生成 WAV

     java SendDataTCP 10.0.0.1 5555 some_song.wav

 

在DS80C400上运行的扬声器应用采用8051汇编编写,需要TINI软件开发套件中免费提供的工具。扬声器应用程序是为地址为400000-47FFFF(十六进制)具有闪存,在地址00000-7FFFF和60000-67FFFF(十六进制)具有RAM的电路板开发的。图 4 描述了该板的内存配置。

扬声器


图4.板内存配置。

其他主板的开发人员需要记住,可能需要更改地址以匹配其主板配置。以下是用于创建扬声器应用程序的生成脚本:

     macro speaker.a51
      a390 -l -Ftbin -d -p 390 speaker.mpp
      java fixBankNum speaker.tbin 66
 

工具宏和 a390 是 TINI SDK 的一部分。fixBankNum 程序是一个小型 Java 应用程序,它更改了用于加载应用程序的目标内存库,并包含在达拉斯半导体 FTP 站点上本应用笔记的源文件中。请注意,“66”是十进制的,因此 speaker.tbin文件将针对位于闪存中的银行 42(十六进制)。

fixBankNum程序是必需的,因为扬声器应用程序不会耗尽闪存,但将程序存储在闪存中是确保在DS80C400电源中断时不会擦除程序的唯一方法。扬声器应用程序不会用完闪存,因为时钟翻了两番,它超过了指定的闪存访问时间。因此,将运行一个小的初始化应用程序,将扬声器应用程序从闪存复制到RAM中。然后,控制跳转到 RAM 中扬声器应用程序的副本,然后启用时钟四倍器并开始正常运行。此初始化应用程序的源称为 init.a51,也包含在本应用笔记的源文件中。使用以下脚本生成初始化应用程序:

    macro init.a51
      a390 -l -Ftbin -d -p 390 init.mpp
 

要运行扬声器应用,必须将初始化和扬声器文件加载到DS80C400上。这是使用JavaKit完成的,JavaKit是TINISDK中包含的另一个应用程序。文档Running_JavaKit.txt(也是 TINI SDK 的一部分)详细介绍了如何运行 JavaKit。上面的构建脚本生成名为speaker.tbin和init.tbin的文件。使用 JavaKit 将这些文件加载到 DS80C400 中。文件应加载到库 41 和 42(十六进制)中。要运行扬声器应用程序,请在 JavaKit 装入器提示符处键入以下内容: 初始化应用程序应将扬声器应用程序复制到内存,打印一些调试,并且扬声器应用程序已启动。运行 SendDataTCP 程序以发送音频数据。经过一两秒钟的音频缓冲后,音乐应该开始。

 B41
      X

 

应用:更改程序参数

扬声器应用程序和主机代码支持以 44.1kHz、22.05kHz 或 11.025kHz 播放单声道数据。选择数据速率时要考虑的权衡是音频质量与网络中断。在低流量网络上,应用程序可能能够以 44.1kHz 的频率播放数据而不会中断。在高流量网络上,音频中的可听见的闪光点可能会变得明显。请按照以下步骤更改采样率:

 

1) 在文件扬声器顶部附近找到等效RELOAD_44_1_at_18.a51。对于 390.44kHz,将此值更改为 1,对于 800.22kHz,将此值更改为 05,对于 1600.11kHz,将此值更改为 025。
2) 在文件顶部附近找到变量 static int audio_quality SendDataTCP.java。将此值更改为MONO_44100、MONO_22050或MONO_11025。
3) 重新编译并重建应用的两个部分,并在DS80C400上重新加载扬声器应用。

 

数据存储在音乐光盘上,以立体声、44.1kHz 16 位样本形式存储。单声道数据仅表示播放一个通道,而不是两个通道。音乐的足够质量是22.05kHz;11.025kHz 足以满足语音数据的需求。

扬声器应用程序的 IP 地址和参数也是可配置的。在 speaker.a51 文件底部附近是以下声明:

network_parameters:
    db  0, 0, 0                                                 ; 3 bytes overhead
    db  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 1         ; ip address
    db  255, 255, 0, 0                                          ; subnet mask
    db  16                                                      ; ipv4 netmask len
    db  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 2         ; gateway
 

该结构的格式在DS80C400用户指南中有描述。但是,请注意,扬声器应用程序中使用的当前 IP 地址为 10.0.0.1,当前网关设置为 10.0.0.2。更改以使应用程序使用不同的 IP 地址应该是微不足道的。在源文件中稍低一点,指定了服务器套接字的端口号:请注意,传递给 SendDataTCP 程序的端口号假定为十六进制值。

address:
    db  0, 0, 0                                             ; overhead
    db  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 1     ; address
    db  55h, 55h                                            ; port
    db  0                                                   ; family

未来:添加第二个频道和其他改进

可以对扬声器应用程序进行许多改进和修改。组播UDP可以取代TCP,允许一台服务器向多个DS80C400广播消息。DHCP 可用于动态获取 IP 地址,从而允许自行配置安装。配置字节可能会告诉扬声器应用程序音频质量是多少,因此它可以轻松地动态播放 11kHz、22kHz 或 44kHz 的音频数据。控制从主机到DS80C400的数据流也有待改进。

另一个关键的改进是增加了另一个音频通道,允许立体声。这里的诀窍是确保添加另一个通道不会使计时器 3 中断例程运行太长。最好的解决方案可能是使用串行端口 0 输出另一个音频通道。应用程序将失去通过串行端口发送调试消息的能力,但计时器中断的额外开销将降至最低。

结论

DS80C400 是支持互联网的扬声器的完美选择。DS80C400的ROM使应用能够以传输原始音频数据的速度通过网络进行通信。通过增加一个16位DAC、一些电阻和少量的焊接工作,DS80C400成为互联网扬声器。

审核编辑:郭婷

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分