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。1,其精度为 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 中实现应用程序。2,或在 Java 中使用 TINI® 运行时环境3.该应用程序足够小,因此在汇编中编写它并不是一项艰巨的任务。
在可能的情况下,扬声器应用程序利用了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.