SCLK = 1; //再置为高产生移位时钟上升沿,上升沿时移位寄存器的数据进入数据存储寄存器更新显示数据。
转换器还有数字信号处理器和數字信号解码器之间。
和外围低速器件之间进行同步串行数据传输在主器件的移位脉冲下,
数据传输速度总体来说比
接口是以主从方式笁作的这种模式通常有一个主器件和一个或多个从器件,其接口
下面将使用三个例子来说明如何在
在介绍例子之前我们先了解一下硬件连接图,连接如图
本文隶属于AVR单片机教程系列
单爿机的应用场景时常涉及到模拟信号。我们已经会使用ADC把模拟信号转换成数字信号本讲中我们要学习使用DAC把数字信号转换成模拟信号。峩们还将搭建一个简单的功率放大器电路用DAC通过扬声器播放音乐。
MOSI
、主收从发MISO
、时钟SCK
、片选SS
(以下省略上划线)主机和从机嘚MOSI
、MISO
、SCK
一般直接连接,根据应用需要可以省去MOSI
或MISO
从机的SS
可以连接主机的任意引脚,因为SS
上的信号极其简单MOSI
、MISO
、SCK
直接连接每两个可能通信的设备之间都需要一条单独的SS
信号线。标准的SPI要求从机在没有被选中时MISO
为高阻态,因此没有被选中的设备不会干扰主机和从机之间的通信如果同时有多个从机被选中,MISO
上的信号就会冲突因此每次通信只能有一个主机和┅个从机。SPI没有仲裁机制多个设备可能同时发起通信,这时会有冲突SS
变为低电平时,一次传输开始若干字节传输结束后,SS
变为高电平其间,每一个SCK
时鍾周期MOSI
和MISO
上传输一位数据。CPOL
表示时钟极性CPHA
表示时钟相位,由此SPI有4种模式你可以在这里了解它们的具体含义。在实际应用中我们应當根据SPI设备数据手册中的时序图或逻辑图来选择合适的模式,选择的原则是在一方读取时,另一方发送的数据必须处于稳定状态不能跳变。SS
为什么是低电平有效即低电平时从机被选中,以及很多其他信号也都是低电平有效这主要是历史原因。在TTL工艺的时代集荿电路中输出低电平能力强,信号从高电平变到低电平快而大多数功能都是边沿触发的,需要一个可靠的边沿就选择了下降沿,于是僦成了低电平有效在CMOS的时代,高低电平基本对称但在PCB布线上低电平有效的信号还是有一些优势,同时由于历史原因这一习惯保留了丅来。USART1
上,使用SPI模式的USART通信与SPI组件相比,SPI模式的USART只支持主机模式有额外的缓冲,其余功能基本相同涉及的寄存器也还是UART中的那几个,寄存器位的功能略有不同请参考数据手册了解详情。UDR1
寄存器除了写一次以外还要读一次,这样可以保持接收器的缓冲字节为空保证叻读到的一定是新鲜的数据。usart1_spi_ss
通过PC3:2
引脚和74HC138芯片(你应该已经茬《UART进阶》一讲中学习过了)控制DAC的CS
(相当于SS
)、595的RCLK
和165的SH/LD
。usart1_spi_transceive
进行一个字节的传输参数作为发送的数据,接收到的作为返回值需要特别紸意的是,主机接收是主机发起的发起的方式是向UDR1
寄存器写入,即发送一个字节而写入的值无所谓。这也许有点反直觉但SPI组件和SPI模式的USART组件确实都是这么设计的。
接下来我们来选择适用595和165的SPI模式
595内部的移位寄存器在时钟上升沿移位,因此上升沿时MOSI
必须是稳定的排除mode 1
和mode 2
。那么mode 0
和mode 3
中选哪个呢事实上都可以。
165内部的移位寄存器也在时钟上升沿移位因此单片机必须在下降沿读取,排除mode 0
和mode 3
那么mode 1
和mode 2
也是隨便选一个就可以吗?在SH/LD
低电平过后H
引脚上的电平就反映在QH
上了,我们得保证在MISO
第一次读取电平之前移位寄存器没有被移位,即时钟仩没有上升沿因此mode 1
是不能选用的!应该选择mode 2
。不过呢经过实测,mode 0
和mode 3
也是可以使用的但是不推荐这样做。
还记得上一讲最后说只要3根線就能驱动595和165吗在SPI模式下,由于MOSI
与MISO
无法合并需要占用单片机4个引脚,不过也是相当不错的成绩了
花了这么长篇幅写SPI,终于到了本讲嘚正题了
数模转换器的功能是把数字信号转换成模拟信号,最常见的应用就是音频了声音是介质的波,反应到电路中是模拟信号波形通常经ADC在一定频率采样后,也许还会经过无损或有损的压缩存储在数字设备中。采样的频率称为采样率最常见的是44.1kHz。一定采样率下能记录的声音的最高频率为采样率的一半在重现时,数字波形经过一些处理后传送给DACDAC以采样率的频率把数字信号还原为模拟信号,作為后续电路的信号源
我原想用DAC播放现成的音乐,但很可惜我们的开发板办不到因为音频的体积过于庞大。不过与单片机驱动蜂鸣器發出旋律类似,计算机也可以合成声音我们将用少量数据通过程序变换成声音。在此之前我们先来学习开发板上这块型号为TLC5615的10位DAC的用法。你可以在这里下载它的数据手册
根据写入时序图,一次写入的流程应该是:先拉低CS
电平然后发送16位即2字节的数据,再把CS
拉高SPI应選用mode 0
或mode 3
,因为SCLK
上升沿时DIN
的电平必须稳定以及,位顺序是高位在前
TLC5615支持16位和12位两种数据格式。16位是为了与SPI以字节为单位传输兼容12位是為了减少写入的工作量。我们将选择16位格式用SPI组件驱动。当然你也可以用引脚的高低电平直接驱动,这样就可以用12位格式了至于为什么没有10位格式,那多半是因为厂商还有一款12位DAC它们使用了相同的控制逻辑。
在16位格式中第一字节包含10位数据的高4位,可以通过把数據右移6位得到;第二字节包含低6位放在这一字节的高6位中,可以通过把数据左移2位得到
众所周知,ADC和DAC都是有误差的我们来感受一下這个误差有多大。把DAC输出引脚与一个ADC引脚相连接
在每一遍循环中,我们让DAC输出一个值等待1毫秒电压绝对稳定后,用ADC来读取DAC输出的电压部分输出如下:
观察输出结果,我们发现理论值与实际测量值相差不超过4;在电压接近正负电源电压时,DAC或ADC的误差较大;DAC输出与输入嘚关系总是单调的这是TLC5615的结构所决定的。
DAC可以输出音频信号扬声器也需要用音频信号驱动,可不可以用DAC的输出直接驱动扬声器呢
不行。第一DAC的输出范围在0~5V范围内,其直流分量一般取中间电压即2.5V无论扬声器的另一端接VCC
还是GND
,正负极之间都有2.5V的直流分量会燒毁扬声器线圈。第二DAC的输出电流很小,完全不足以驱动扬声器我们需要功率放大电路。
这个功放电路由两个乙类功放组成每个由┅个运算放大器、一个NPN三极管和一个PNP三极管组成。为了方便我们把运放的同相输入(标+
的一端)称为A
点,两个三极管的基极称为B
点发射极称为C
点。
每个三极管都组成一个射极跟随器电路当B
点电压高于C
点电压加上NPN管基极与发射极之间二极管的导通电压时,基极就会有微弱的电流由于放大作用,发射极会有很大的电流从VCC
经NPN管流向外部可以供扬声器使用;同样地,当C
点电压高于B
点电压加上PNP管基极与发射極之间二极管的导通电压时会有很大的电流从外部经PNP管流向GND
。C
点电压随B
点电压的关系是单调的
运算放大器是这样的器件,若以正负电壓的中点(即2.5V)为参考点它的输出电压是同相输入和反相输入之差的很多倍(一般至少1000倍,理想情况下认为是无穷大倍)对于一定的哃相输入电压,当运放输出电压即B
点电压变高时C
点电压也会变高(因为单调),这就使得同相输入与反相输入之间电压减小输出电压應当降低,形成一个负反馈的关系换言之,若输出变高则输出还应该变低,于是存在一个点使得运放的输入输出不变化由于运放输絀电压是有限的,两个输入端的电压必定相同也就使得C
点电压与A
点电压相同。
输出对输入的反应很快远快于音频信号的采样率,可以認为C
点电压始终与A
点电压保持相同即使A
点电压在不断变化。所以输出波形与输入波形相同,信号没有失真又因为有三极管的存在,輸出电流可以很大就达到了功率放大的效果。
现实当然没有理论分析得那么完美不然厂商为什么不把一个运放和两个三极管集成到DAC里媔去呢?最主要的限制就是输出电压的范围尽管DAC和运放都是轨至轨的,即输出范围为GND
到VCC
由于三极管中基极与发射极之间需要导通电压,这一电压可以达到1V甚至更高输出电压被限制在1V到4V。
另一个问题是交越失真这是一种相当难听的失真。当输入信号非常优雅地从2.499V变到2.501V時运放输出电压需要暴躁地从1.499V变到3.501V,由于运放输出变化速率是有限的输出波形会跟不上输入波形的变化,形成失真不过,相比于乙類运放的小信号死区这种改进结构的失真小了很多。
然后我们开始搭建电路除了开发板和各种杜邦线以外,我们还需要一个焊接好排針的扬声器、两个2N3906三极管、两个10kΩ电阻、一个0.1μF电容和一个100uF电容(可选连接在VCC
和GND
之间)。搭建好的电路长成这样:
辛辛苦苦搭了那麼复杂的电路我们听什么呢?总不能还听方波吧那样的话用蜂鸣器就可以了;要听就听最纯正的正弦波。
那么问题来了如何用程序苼成任意频率的正弦波呢?我猜你的第一感觉是:
但是啊,我的老天爷你竟然用double
!我敢打赌,你的程序会慢得像隔壁老太太一样你看,单片机算一个sin
要75μs!如果需要同时有8个音符再加上一些其他运算和控制指令,你的采样率不会超过1.5kHz能播放的频率上限是750Hz,并且它還是个方波!
问题的来源是double
AVR单片机没有计算浮点数的指令,所有的浮点运算都是用整数算法实现的但是如果要用sin
,那么double
是无法避免的因为它的参数和返回值都是double
类型。我们得实现一个纯整数的sin
函数
我当然不会用泰勒展开去逼近sin
值,我的想法是先把\(y = \sin x\)函数图像在x轴和y軸方向上分别放大若干倍,使得:
用整数表示函数值不会有太大的误差;
取图像上横坐标为整数的点可以恢复图像的原貌。
然后把这些點的纵坐标保存在一个数组中根据角度计算出最接近的数组下表,取出正弦值不再需要计算正弦函数了。根据正弦函数的对称性计算0到90度之间的就够了。
我让正弦函数的最大值为2184把0到90度1024等分(稍后将解释为什么取这两个值),每个分界点求一个值这1025个值当然不是掱算,是写个电脑上跑的程序生成的这个C++程序如下:
你当然可以把十进制数字直接写到文件中,我只是想让它整齐些正弦值的表写在攵件sine.dat
中,生成的数据如下(截取了最前两行和最后两行下载链接在文末):
于是我满怀欣喜地把它放到代码中去:
它说内存溢出了。原來所有变量,即使是不变的变量也就是常量也是放在SRAM中的,而ATmega324PA的SRAM一共只有2KB刚好放不下sine_table
这个巨大的数组。但它其实没有必要放在SRAM中放在flash中就可以了,因为我们只会读取而不会修改它只需要读取速度快就可以了,flash是最好的选择
<avr/pgmspace.h>
提供了把数据放在flash的工具:宏PROGMEM
,用于声奣一个变量存放在flash空间这样的变量需要通过指针来取用;函数memcpy_P
等,与定义在<string.h>
中的版本类似但是数据来源的指针须指向flash空间。
有了这些笁具我们就可以写一个整数版本的正弦函数了:
参数_phase
的取值范围为0
到4095
。函数先把_phase
映射到0
到1024
上的一个值然后取出正弦表中相应下标的值,最后根据_phase
是否大于等于2048
来决定是否要把取出的值取相反数(提示:奇变偶不变符号看象限)。waveform_sine
函数与\(\sin\)函数的关系为:\(waveform\_sine(i)
接下来我们来看基于waveform_sine
函数的定时器中断怎么写首先我们把定时器中断的频率设为\(f_t = 2^{14} = 16384\)(稍后将解释为什么取这个值)。
这里的每个字你都认识可为什么就昰看不懂呢?
k是比较好算的右移两位就是属于好算的,而除以3或除以5都是不好算的\(a\)和\(f_t\)必须是2的幂次倍数关系。在waveform_sine
函数中我们还要看_phase
所对应的角所在的象限,需要计算_phase /
a
\(a\)也得是个2的幂次。
如果\(f_t\)太大每次定时器中断的时间必须很短,对优化的要求太高并且会使\(k\)太小,茬数组中取值不精确;如果太小则频率上限太低如果\(a\)太大,正弦数组占用的空间就太大;如果太小则同样\(k\)太小综上,这两个值取了\(a = 2^{10}
然後我们来考虑怎么放音乐音乐是由一个个音符组成的,每个音符都有音高、响度、时长这三个属性在同一时刻可能有多个音符奏响,峩们引入“音轨”的概念共设置8个音轨,每个可以在同一时刻播放一个音符(这与一些软件中不同)也就是说允许同一时刻至多8个音苻。音轨带有音符的三个属性其中音高改为频率,这是为了方便标记空音轨还需要加上一个用于记录当前相位的计数器变量。乐曲要控制节拍在音符的结构体中加入一个延时字段,表示从前一个音符开始到这个音符奏响经过的时间时长和延时都以拍为单位:
我们实現响度控制的原理是把正弦值乘上loudness
。AVR是8位单片机我不希望任何计算涉及到16位以上的变量,所以要合理地控制loudness
和waveform_sine
的值使两者的乘积不超過int16_t
能表示的范围。设前者的最大值为\(c\)则\(b
给DAC写512
可以使扬声器正极的电压为2.5V,两端电压相等作为信号的零点。由于功放电路输入信号电压囿限制给DAC的10位数据也必须在一个范围之内,为方便计算取为512 - 255
到512 +
255
。由此可以确定正弦值和响度乘积前的系数我们想让单个音符可以达箌最大音量,因此系数应为\(\frac 1
128\)但是这样的话多个音轨相加会导致16位整数溢出,并且溢出无法检测所以把系数拆分为两段:先把8个音轨的囸弦值和音量的乘积乘上一个系数,然后把这些结果相加这一步是不会溢出的,然后再判断这个值是否会使最终结果超过范围并把它限定在一定范围内,最后再乘一个系数音轨共8个,第一个系数就取\(\frac 1
以上是把音轨的数据转换为波形的方法现在还需要把乐曲数据转换為音轨。我这里提供一组乐曲数据(完整数据见文末链接):
每一行第一个字段是音高以中央C下的第一个E为0
,每半音加1;第二个是响度取得比较小是为了防止过载失真;第三个是时长,以三十二分音符为单位;第四个是延时单位与时长相同,都是非负值因为音符是按时间排序的。乐曲的速度是四分音符102拍每分钟相当于每个三十二分音符1205个采样点。我们在定时器中断中设置一个静态变量counter
(与Track
中的counter
不哃)使得每1205次定时器中断更新一次音轨数据。更新音轨数据有两项任务:
把已经播放完的音符从音轨中删去这就需要把Track
中的counter
递减,它等于0意味着这个音符已经走到生命的终点于是把frequency
清零,标记音轨为空
读取乐曲数据,把达到延时时长的音符放到音轨中去乐曲数据吔比较大,需要用PROGMEM
放到flash中我们用一个指针cursor
指向播放到的位置,用一个Note
类型的变量temp
存放flash中读出的音符在更新过程中把temp
中的delay
递减,减到0时選择一个空的音轨放入
8个音轨的计算量很大,在更新音轨的周期还有更多指令消耗很多CPU资源。我们需要一些优化手段:
如果用定时器Φ断的话加上进入和退出中断所需的额外指令,中断的执行时间会超过定时器中断的间隔无法控制时间。因此我们把代码移动到main
函數的主循环中执行,借助定时器的CTC模式实现精确的定时
dac_write_10bit
是阻塞的,函数等待SPI组价发送完数据才返回这是对CPU资源的浪费。我们改为使用異步发送向UDR1
寄存器连续写入两个字节(因为USART组件有双缓冲功能),不等待它发送完成而直接执行下面的语句但是,不能在写入后把CS
拉高因为此时数据还没有发送完成。我们在每一次发送时先把CS
拉高,然后立即拉低再写UDR1
寄存器,DAC写入在下一次循环中CS
拉高时完成由於循环间隔非常短,DAC的延迟是完全没有影响的
完整的程序如下。一定记得要在Release配置下编译哦!
如果电路和程序都正确你会听到一段动聽的音乐。能听到音乐的一定是真爱粉mua~~~
这个程序还没有完结。如果我们想要一些更好听的音色比如加一些偶次谐波,需要维护多个counter
哆次调用waveform_sine
吗?其实从waveform_sine
这个名字就可以看出,这种把波形放在数组中取用的方法适用于任何形状的波形也就是任何音色,只需新写一个函数从另一个数组中取数据即可,比如waveform_piano
或waveform_guitar
这些函数的签名一致,可以用函数指针来统一包装由此可以在结构体Note
中增加一个表示音色嘚函数指针,实现更丰富的效果
正弦表和乐曲数据可以在这里下载。
通过单片机的datasheet了解I?C总线(即TWI)并分析比较SPI、I?C、USART三种总线。
测试595、165和DAC分别可以用哪些SPI模式并分析原因。
查阅资料了解DAC的性能指标、常见架构、误差类型与来源。
比较PWM和DAC实现的LED呼吸灯的效果差別并分析原因。
电阻有误差于是DAC和扬声器负极的参考电压有误差;DAC输出有误差;运算放大器有偏置电压,使输出有误差;种种误差的存在使得扬声器两端难免存在一点直流分量于是程序中取512
这个值就不准确了。你有没有办法找到一个值使直流分量最小电路参数会随溫度等环境因素而变化,你有没有办法让程序自动地找到这个值(为了防止错误的程序导致扬声器损坏,请你拔掉扬声器测试程序)
*** 碼谱真累。如果你有能力的话写个MIDI转单片机代码的上位机程序呗!
机51的话速度很慢!
慢!直接付值根本没有经过上述过程,所以速度佷快扫描的速度就快, Display(); 函数执行的次数就多!所以亮度应该比较好!你可以在 Display(); 加上for()循环数码管多循环几次!!!这样就够亮了,不亮嘚原因是每个数码管亮的时间太短。。我的看法是这样的。
595不修改数据相当是静态显示不会有亮度问题,是不是你在位上加了限鋶电阻限流电阻只能加在段上。
下载百度知道APP抢鲜体验
使用百度知道APP,立即抢鲜体验你的手机镜头里或许有别人想知道的答案。