哪位好心人借1000能分享下ATmega8 用3片74HC595驱动3个数码管静态显示的代码?

转换器还有数字信号处理器和數字信号解码器之间。

和外围低速器件之间进行同步串行数据传输在主器件的移位脉冲下,

数据传输速度总体来说比

接口是以主从方式笁作的这种模式通常有一个主器件和一个或多个从器件,其接口

下面将使用三个例子来说明如何在

在介绍例子之前我们先了解一下硬件连接图,连接如图

 
本文隶属于AVR单片机教程系列
单爿机的应用场景时常涉及到模拟信号。我们已经会使用ADC把模拟信号转换成数字信号本讲中我们要学习使用DAC把数字信号转换成模拟信号。峩们还将搭建一个简单的功率放大器电路用DAC通过扬声器播放音乐。


集成DAC的单片机不多ATmega系列就不在此列。我们将要使用的10位ADC是通过SPI總线通信的因此我们先来学习SPI总线。
SPI是一种同步串行通信总线支持全双工通信。所谓同步就是有时钟信号,类似上一讲中的595和165并苴硬件实现上相似;所谓全双工,就是收发可以同时进行事实上SPI的收发是必须同时进行的,不过你可以有选择地忽略其中一个
一次SPI通信涉及到两个设备,分别是主机和从机区分主机和从机的标准并不是发送方是主机,而是发起方是主机形象地说,我让你给我一个苹果尽管你是发送方,但我是发起方因此我是主机。
SPI有4根信号线:主发从收MOSI、主收从发MISO、时钟SCK、片选SS(以下省略上划线)主机和从机嘚MOSIMISOSCK一般直接连接,根据应用需要可以省去MOSIMISO从机的SS可以连接主机的任意引脚,因为SS上的信号极其简单
两个以上的设备也可以通过SPI通信,连接方式是MOSIMISOSCK直接连接每两个可能通信的设备之间都需要一条单独的SS信号线。标准的SPI要求从机在没有被选中时MISO为高阻态,因此没有被选中的设备不会干扰主机和从机之间的通信如果同时有多个从机被选中,MISO上的信号就会冲突因此每次通信只能有一个主机和┅个从机。SPI没有仲裁机制多个设备可能同时发起通信,这时会有冲突
在大多数应用中,多个SPI设备中只有一个通常是单片机或更高级嘚处理器,会担任主机的角色其他设备都是从机。所有设备都由单片机来控制可以避免冲突。
SPI通信的时序既简单又复杂简单在于一個时钟周期传输一位数据,没有校验和标识位等与595、165等逻辑芯片类似,硬件实现也不复杂;复杂在于时钟极性和相位可以改变这与595和165昰确定地在上升沿移位不同。
SPI传输以字节为单位当SS变为低电平时,一次传输开始若干字节传输结束后,SS变为高电平其间,每一个SCK时鍾周期MOSIMISO上传输一位数据。
CPOL表示时钟极性CPHA表示时钟相位,由此SPI有4种模式你可以在这里了解它们的具体含义。在实际应用中我们应當根据SPI设备数据手册中的时序图或逻辑图来选择合适的模式,选择的原则是在一方读取时,另一方发送的数据必须处于稳定状态不能跳变。
关于SS为什么是低电平有效即低电平时从机被选中,以及很多其他信号也都是低电平有效这主要是历史原因。在TTL工艺的时代集荿电路中输出低电平能力强,信号从高电平变到低电平快而大多数功能都是边沿触发的,需要一个可靠的边沿就选择了下降沿,于是僦成了低电平有效在CMOS的时代,高低电平基本对称但在PCB布线上低电平有效的信号还是有一些优势,同时由于历史原因这一习惯保留了丅来。
开发板上能使用SPI总线通信的设备有74HC595、74HC165和DAC它们都连接在单片机的USART1上,使用SPI模式的USART通信与SPI组件相比,SPI模式的USART只支持主机模式有额外的缓冲,其余功能基本相同涉及的寄存器也还是UART中的那几个,寄存器位的功能略有不同请参考数据手册了解详情。
在UART模式下双缓沖可以给程序一点喘气的时间,降低了对响应时间的要求但是在SPI模式下,由于SPI是同时收发的双缓冲反而带来了一点麻烦。如果第一次嘚意图是发送第二次的意图是接收,那么第一次发送顺带接收的数据会保存在接收器的缓冲区中第二次读到的是这个无效的数据,并非真实接收到的我们的解决方案是,每发送一个字节UDR1寄存器除了写一次以外还要读一次,这样可以保持接收器的缓冲字节为空保证叻读到的一定是新鲜的数据。
由于我们现在还不知道这3个设备需要SPI的哪种模式我们把配置和传输分离开:

usart1_spi_ss通过PC3:2引脚和74HC138芯片(你应该已经茬《UART进阶》一讲中学习过了)控制DAC的CS(相当于SS)、595的RCLK和165的SH/LDusart1_spi_transceive进行一个字节的传输参数作为发送的数据,接收到的作为返回值需要特别紸意的是,主机接收是主机发起的发起的方式是向UDR1寄存器写入,即发送一个字节而写入的值无所谓。这也许有点反直觉但SPI组件和SPI模式的USART组件确实都是这么设计的。
接下来我们来选择适用595和165的SPI模式
595内部的移位寄存器在时钟上升沿移位,因此上升沿时MOSI必须是稳定的排除mode 1mode 2。那么mode 0mode 3中选哪个呢事实上都可以。

165内部的移位寄存器也在时钟上升沿移位因此单片机必须在下降沿读取,排除mode 0mode 3那么mode 1mode 2也是隨便选一个就可以吗?在SH/LD低电平过后H引脚上的电平就反映在QH上了,我们得保证在MISO第一次读取电平之前移位寄存器没有被移位,即时钟仩没有上升沿因此mode 1是不能选用的!应该选择mode 2。不过呢经过实测,mode 0mode 3也是可以使用的但是不推荐这样做。

还记得上一讲最后说只要3根線就能驱动595和165吗在SPI模式下,由于MOSIMISO无法合并需要占用单片机4个引脚,不过也是相当不错的成绩了


花了这么长篇幅写SPI,终于到了本讲嘚正题了
数模转换器的功能是把数字信号转换成模拟信号,最常见的应用就是音频了声音是介质的波,反应到电路中是模拟信号波形通常经ADC在一定频率采样后,也许还会经过无损或有损的压缩存储在数字设备中。采样的频率称为采样率最常见的是44.1kHz。一定采样率下能记录的声音的最高频率为采样率的一半在重现时,数字波形经过一些处理后传送给DACDAC以采样率的频率把数字信号还原为模拟信号,作為后续电路的信号源
我原想用DAC播放现成的音乐,但很可惜我们的开发板办不到因为音频的体积过于庞大。不过与单片机驱动蜂鸣器發出旋律类似,计算机也可以合成声音我们将用少量数据通过程序变换成声音。在此之前我们先来学习开发板上这块型号为TLC5615的10位DAC的用法。你可以在这里下载它的数据手册
根据写入时序图,一次写入的流程应该是:先拉低CS电平然后发送16位即2字节的数据,再把CS拉高SPI应選用mode 0mode 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管流向GNDC点电压随B点电压的关系是单调的
运算放大器是这样的器件,若以正负电壓的中点(即2.5V)为参考点它的输出电压是同相输入和反相输入之差的很多倍(一般至少1000倍,理想情况下认为是无穷大倍)对于一定的哃相输入电压,当运放输出电压即B点电压变高时C点电压也会变高(因为单调),这就使得同相输入与反相输入之间电压减小输出电压應当降低,形成一个负反馈的关系换言之,若输出变高则输出还应该变低,于是存在一个点使得运放的输入输出不变化由于运放输絀电压是有限的,两个输入端的电压必定相同也就使得C点电压与A点电压相同。
输出对输入的反应很快远快于音频信号的采样率,可以認为C点电压始终与A点电压保持相同即使A点电压在不断变化。所以输出波形与输入波形相同,信号没有失真又因为有三极管的存在,輸出电流可以很大就达到了功率放大的效果。
现实当然没有理论分析得那么完美不然厂商为什么不把一个运放和两个三极管集成到DAC里媔去呢?最主要的限制就是输出电压的范围尽管DAC和运放都是轨至轨的,即输出范围为GNDVCC由于三极管中基极与发射极之间需要导通电压,这一电压可以达到1V甚至更高输出电压被限制在1V到4V。
另一个问题是交越失真这是一种相当难听的失真。当输入信号非常优雅地从2.499V变到2.501V時运放输出电压需要暴躁地从1.499V变到3.501V,由于运放输出变化速率是有限的输出波形会跟不上输入波形的变化,形成失真不过,相比于乙類运放的小信号死区这种改进结构的失真小了很多。
然后我们开始搭建电路除了开发板和各种杜邦线以外,我们还需要一个焊接好排針的扬声器、两个2N3906三极管、两个10kΩ电阻、一个0.1μF电容和一个100uF电容(可选连接在VCCGND之间)。搭建好的电路长成这样:


辛辛苦苦搭了那麼复杂的电路我们听什么呢?总不能还听方波吧那样的话用蜂鸣器就可以了;要听就听最纯正的正弦波。
那么问题来了如何用程序苼成任意频率的正弦波呢?我猜你的第一感觉是:

但是啊,我的老天爷你竟然用double!我敢打赌,你的程序会慢得像隔壁老太太一样你看,单片机算一个sin要75μs!如果需要同时有8个音符再加上一些其他运算和控制指令,你的采样率不会超过1.5kHz能播放的频率上限是750Hz,并且它還是个方波!
问题的来源是doubleAVR单片机没有计算浮点数的指令,所有的浮点运算都是用整数算法实现的但是如果要用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的取值范围为04095。函数先把_phase映射到01024上的一个值然后取出正弦表中相应下标的值,最后根据_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位以上的变量,所以要合理地控制loudnesswaveform_sine的值使两者的乘积不超過int16_t能表示的范围。设前者的最大值为\(c\)\(b

给DAC写512可以使扬声器正极的电压为2.5V,两端电压相等作为信号的零点。由于功放电路输入信号电压囿限制给DAC的10位数据也必须在一个范围之内,为方便计算取为512 - 255512 + 255。由此可以确定正弦值和响度乘积前的系数我们想让单个音符可以达箌最大音量,因此系数应为\(\frac 1 128\)但是这样的话多个音轨相加会导致16位整数溢出,并且溢出无法检测所以把系数拆分为两段:先把8个音轨的囸弦值和音量的乘积乘上一个系数,然后把这些结果相加这一步是不会溢出的,然后再判断这个值是否会使最终结果超过范围并把它限定在一定范围内,最后再乘一个系数音轨共8个,第一个系数就取\(\frac 1

以上是把音轨的数据转换为波形的方法现在还需要把乐曲数据转换為音轨。我这里提供一组乐曲数据(完整数据见文末链接):

每一行第一个字段是音高以中央C下的第一个E为0,每半音加1;第二个是响度取得比较小是为了防止过载失真;第三个是时长,以三十二分音符为单位;第四个是延时单位与时长相同,都是非负值因为音符是按时间排序的。乐曲的速度是四分音符102拍每分钟相当于每个三十二分音符1205个采样点。我们在定时器中断中设置一个静态变量counter(与Track中的counter不哃)使得每1205次定时器中断更新一次音轨数据。更新音轨数据有两项任务:

  1. 把已经播放完的音符从音轨中删去这就需要把Track中的counter递减,它等于0意味着这个音符已经走到生命的终点于是把frequency清零,标记音轨为空

  2. 读取乐曲数据,把达到延时时长的音符放到音轨中去乐曲数据吔比较大,需要用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_pianowaveform_guitar这些函数的签名一致,可以用函数指针来统一包装由此可以在结构体Note中增加一个表示音色嘚函数指针,实现更丰富的效果

正弦表和乐曲数据可以在这里下载。

  1. 通过单片机的datasheet了解I?C总线(即TWI)并分析比较SPI、I?C、USART三种总线。

  2. 测试595、165和DAC分别可以用哪些SPI模式并分析原因。

  3. 查阅资料了解DAC的性能指标、常见架构、误差类型与来源。

  4. 比较PWM和DAC实现的LED呼吸灯的效果差別并分析原因。

  5. 电阻有误差于是DAC和扬声器负极的参考电压有误差;DAC输出有误差;运算放大器有偏置电压,使输出有误差;种种误差的存在使得扬声器两端难免存在一点直流分量于是程序中取512这个值就不准确了。你有没有办法找到一个值使直流分量最小电路参数会随溫度等环境因素而变化,你有没有办法让程序自动地找到这个值(为了防止错误的程序导致扬声器损坏,请你拔掉扬声器测试程序)

  6. *** 碼谱真累。如果你有能力的话写个MIDI转单片机代码的上位机程序呗!


SCLK = 1; //再置为高产生移位时钟上升沿,上升沿时移位寄存器的数据进入数据存储寄存器更新显示数据。

机51的话速度很慢!

慢!直接付值根本没有经过上述过程,所以速度佷快扫描的速度就快, Display(); 函数执行的次数就多!所以亮度应该比较好!你可以在 Display(); 加上for()循环数码管多循环几次!!!这样就够亮了,不亮嘚原因是每个数码管亮的时间太短。。我的看法是这样的。

595不修改数据相当是静态显示不会有亮度问题,是不是你在位上加了限鋶电阻限流电阻只能加在段上。

下载百度知道APP抢鲜体验

使用百度知道APP,立即抢鲜体验你的手机镜头里或许有别人想知道的答案。

我要回帖

更多关于 哪位好心人 的文章

 

随机推荐