历史上的今天
返回首页

历史上的今天

今天是:2024年09月08日(星期日)

2020年09月08日 | AVR单片机教程——DAC

2020-09-08 来源:eefocus

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


SPI总线

集成DAC的单片机不多,ATmega系列就不在此列。我们将要使用的10位ADC是通过SPI总线通信的,因此我们先来学习SPI总线。


SPI是一种同步串行通信总线,支持全双工通信。所谓同步,就是有时钟信号,类似上一讲中的595和165,并且硬件实现上相似;所谓全双工,就是收发可以同时进行,事实上SPI的收发是必须同时进行的,不过你可以有选择地忽略其中一个。


一次SPI通信涉及到两个设备,分别是主机和从机。区分主机和从机的标准并不是发送方是主机,而是发起方是主机。形象地说,我让你给我一个苹果,尽管你是发送方,但我是发起方,因此我是主机。


SPI有4根信号线:主发从收MOSI、主收从发MISO、时钟SCK、片选SS(以下省略上划线)。主机和从机的MOSI、MISO、SCK一般直接连接,根据应用需要可以省去MOSI或MISO,从机的SS可以连接主机的任意引脚,因为SS上的信号极其简单。


两个以上的设备也可以通过SPI通信,连接方式是MOSI、MISO、SCK直接连接,每两个可能通信的设备之间都需要一条单独的SS信号线。标准的SPI要求从机在没有被选中时,MISO为高阻态,因此没有被选中的设备不会干扰主机和从机之间的通信。如果同时有多个从机被选中,MISO上的信号就会冲突,因此每次通信只能有一个主机和一个从机。SPI没有仲裁机制,多个设备可能同时发起通信,这时会有冲突。


在大多数应用中,多个SPI设备中只有一个,通常是单片机或更高级的处理器,会担任主机的角色,其他设备都是从机。所有设备都由单片机来控制,可以避免冲突。


SPI通信的时序既简单又复杂。简单在于一个时钟周期传输一位数据,没有校验和标识位等,与595、165等逻辑芯片类似,硬件实现也不复杂;复杂在于时钟极性和相位可以改变,这与595和165是确定地在上升沿移位不同。

SPI传输以字节为单位。当SS变为低电平时,一次传输开始。若干字节传输结束后,SS变为高电平。其间,每一个SCK时钟周期,MOSI和MISO上传输一位数据。


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的哪种模式,我们把配置和传输分离开:


#include

#include

#include


#define UCPHA1 1 // macros not found in

#define UDORD1 2


typedef enum

{

    SPI_SS_DAC  = 0b00,

    SPI_SS_NONE = 0b01,

    SPI_SS_595  = 0b10,

    SPI_SS_165  = 0b11

} spi_ss_t;


void usart1_spi_ss(spi_ss_t _which)

{

    PORTC = (PORTC & ~(0b11 << PORTC2)) // protect other bits

          | (_which & 0b11) << PORTC2;  // configure PORTC3:2 bits

}


void usart1_spi_init()

{

    reset_bit(DDRD, PORTD2);  // RXD1/MISO input

    set_bit(DDRD, PORTD3);    // TXD1/MOSI output

    set_bit(DDRD, PORTD4);    // XCK1/SCK  output

    UCSR1B =    1 << RXEN1    // enable receiver

           |    1 << TXEN1;   // enable transmitter

    UCSR1C = 0b11 << UMSEL10; // Master SPI mode

    usart1_spi_ss(SPI_SS_NONE);

    DDRC  |= 0b11 << DDC2;    // PC3:2 output, select none

}


typedef enum

{

    SPI_CPOL_0    = 0 << UCPOL1, SPI_CPOL_1    = 1 << UCPOL1,

    SPI_CPHA_0    = 0 << UCPHA1, SPI_CPHA_1    = 1 << UCPHA1,

    SPI_MSB_FIRST = 0 << UDORD1, SPI_LSB_FIRST = 1 << UDORD1,

} spi_config_t;


void usart1_spi_mode(spi_config_t _config)

{

    cond_bit(read_bit(_config, UCPOL1), UCSR1C, UCPOL1);

    cond_bit(read_bit(_config, UCPHA1), UCSR1C, UCPHA1);

    cond_bit(read_bit(_config, UDORD1), UCSR1C, UDORD1);

}


uint8_t usart1_spi_transceive(uint8_t _send)

{

    UDR1 = _send;                   // start a transmission

    while (!read_bit(UCSR1A, TXC1)) // wait until transmission finishes

        ;

    set_bit(UCSR1A, TXC1);          // clear TXC1 bit

    return UDR1;                    // clear the buffer

}

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中选哪个呢?事实上都可以。


void write_595_spi(uint8_t _data)

{

    usart1_spi_mode(SPI_CPOL_0 | SPI_CPHA_0 | SPI_LSB_FIRST);

              // or SPI_CPOL_1 | SPI_CPHA_1

    usart1_spi_transceive(_data);

    usart1_spi_ss(SPI_SS_595);

    usart1_spi_ss(SPI_SS_NONE);

}

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


uint8_t read_165_spi()

{

    usart1_spi_mode(SPI_CPOL_1 | SPI_CPHA_0 | SPI_LSB_FIRST);

    usart1_spi_ss(SPI_SS_165);

    usart1_spi_ss(SPI_SS_NONE);

    return usart1_spi_transceive(0);

}

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


DAC

花了这么长篇幅写SPI,终于到了本讲的正题了。


数模转换器的功能是把数字信号转换成模拟信号,最常见的应用就是音频了。声音是介质的波,反应到电路中是模拟信号波形,通常经ADC在一定频率采样后,也许还会经过无损或有损的压缩,存储在数字设备中。采样的频率称为采样率,最常见的是44.1kHz。一定采样率下能记录的声音的最高频率为采样率的一半。在重现时,数字波形经过一些处理后传送给DAC,DAC以采样率的频率把数字信号还原为模拟信号,作为后续电路的信号源。


我原想用DAC播放现成的音乐,但很可惜我们的开发板办不到,因为音频的体积过于庞大。不过,与单片机驱动蜂鸣器发出旋律类似,计算机也可以合成声音,我们将用少量数据通过程序变换成声音。在此之前,我们先来学习开发板上这块型号为TLC5615的10位DAC的用法。你可以在这里下载它的数据手册。


TLC5615共有8个引脚:电源VDD、AGND;数字DIN、SCLK、CS、DOUT;模拟REFIN、OUT。REFIN连接了2.5V的参考电压,因此输出电压为INPUT1024×5V,覆盖了GND到VCC之间的电压。


CS连接74HC138的一个输出,SCLK连接SCK,DIN连接MOSI,DOUT未连接。

根据写入时序图,一次写入的流程应该是:先拉低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位得到。


void write_dac_spi(uint16_t _data)

{

    usart1_spi_mode(SPI_CPOL_0 | SPI_CPHA_0 | SPI_MSB_FIRST);

    usart1_spi_ss(SPI_SS_DAC);

    usart1_spi_transceive(_data >> 6);

    usart1_spi_transceive(_data << 2);

    usart1_spi_ss(SPI_SS_NONE);

}

众所周知,ADC和DAC都是有误差的,我们来感受一下这个误差有多大。把DAC输出引脚与一个ADC引脚相连接。


#include

#include

#include

#include


int main(void)

{

    dac_init();

    adc_init();

    uart_init(UART_TX_64, 384);

    for (uint16_t i = 0; i != 1 << 10; ++i)

    {

        dac_write_10bit(i);

        delay(1);

        uint16_t adc = adc_read_10bit(ADC_0);

        uart_set_align(ALIGN_RIGHT, 4, '0');

        uart_print_int(i);

        uart_print_string(" ");

        uart_set_align(ALIGN_RIGHT, 4, '0');

        uart_print_int(adc);

        uart_print_line();

    }

    while (1)

        ;

}

在每一遍循环中,我们让DAC输出一个值,等待1毫秒电压绝对稳定后,用ADC来读取DAC输出的电压。部分输出如下:


0000 0000

0001 0000

0002 0000

0003 0001

0004 0002

0005 0003

0006 0004

0007 0005

0008 0006

0009 0007

...

0507 0507

0508 0508

0509 0510

0510 0510

0511 0512

0512 0512

0513 0513

0514 0514

0515 0515

0516 0516

...

1014 1018

1015 1019

1016 1020

1017 1021

1018 1021

1019 1022

1020 1023

1021 1023

1022 1023

1023 1023

观察输出结果,我们发现,理论值与实际测量值相差不超过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之间)。搭建好的电路长成这样:

播放器

辛辛苦苦搭了那么复杂的电路,我们听什么呢?总不能还听方波吧,那样的话用蜂鸣器就可以了;要听就听最纯正的正弦波。


那么问题来了,如何用程序生成任意频率的正弦波呢?我猜,你的第一感觉是:


#include

#include


static const double timer_freq;

static const double note_freq;


ISR(TIMER1_COMPA_vect)

{

    static double phase = 0;

    double sine = sin(phase);

    phase += note_freq / timer_freq * M_PI * 2;

    if (phase > M_PI * 2)

        phase -= M_PI * 2;

    // ...

}

我们把计算正弦值的过程放在定时器中断中进行,它的频率为timer_freq(ft),在这个应用中也就是采样率。要播放的音符的频率为note_freq(fn),phase表示这个正弦波的当前相位。这个声波的方程为y=Asin(2πfnt),在时间间隔Δt=1ft内,相位变化了Δφ=2πfnft。程序的逻辑十分正确。


但是啊,我的老天爷,你竟然用double!我敢打赌,你的程序会慢得像隔壁老太太一样。你看,单片机算一个sin要75μs!如果需要同时有8个音符,再加上一些其他运算和控制指令,你的采样率不会超过1.5kHz,能播放的频率上限是750Hz,并且它还是个方波!


问题的来源是double,AVR单片机没有计算浮点数的指令,所有的浮点运算都是用整数算法实现的。但是如果要用sin,那么double是无法避免的,因为它的参数和返回值都是double类型。我们得实现一个纯整数的sin函数。


我当然不会用泰勒展开去逼近sin值,我的想法是,先把y=sinx函数图像在x轴和y轴方向上分别放大若干倍,使得:


用整数表示函数值不会有太大的误差;


取图像上横坐标为整数的点,可以恢复图像的原貌。


然后把这些点的纵坐标保存在一个数组中,根据角度计算出最接近的数组下表,取出正弦值,不再需要计算正弦函数了。根据正弦函数的对称性,计算0到90度之间的就够了。


我让正弦函数的最大值为2184,把0到90度1024等分(稍后将解释为什么取这两个值),每个分界点求一个值。这1025个值当然不是手算,是写个电脑上跑的程序生成的。这个C++程序如下:


#include

#include

#include

#include

#include


std::ofstream file("sine.dat");


void print(int phase)

推荐阅读

史海拾趣

G24 Innovations公司的发展小趣事

远阳(FLYOUNG)公司创立于21世纪初,正值中国电子行业快速发展的黄金时期。公司创始人李先生,凭借在电子行业多年的技术积累和敏锐的市场洞察力,决定创立一家专注于数据工程电缆研发与生产的公司。初期,远阳面临着资金短缺和技术瓶颈的双重挑战。但李先生带领团队夜以继日地研发,终于成功推出了首款高性能HDMI线,其卓越的品质迅速赢得了市场的认可。这一技术创新不仅为公司赢得了第一批忠实客户,也为远阳后续的技术发展奠定了坚实基础。

BAHCO公司的发展小趣事

在市场竞争日益激烈的背景下,BAHCO选择了与同样拥有悠久历史的美国Snap-on公司进行合作。这次强强联合不仅加强了双方在技术、市场等方面的互补优势,更为BAHCO打开了更广阔的市场空间。通过Snap-on的全球销售网络,BAHCO的产品得以进入更多国家和地区,进一步提升了公司的品牌影响力和市场竞争力。

EEMB Co Ltd公司的发展小趣事

EEMB集团成立于1995年,初期以外销为主,总部位于武汉东西湖金银湖畔。公司创始人们凭借对电子行业的敏锐洞察力和对锂电池技术的深入研究,决定专注于锂电池的研发与生产。他们组建了一支技术实力强大的研发团队,并开始着手设计和生产高性能的锂电池产品。在初期的几年里,公司逐渐在市场上建立起了一定的声誉,并开始与一些知名的工业企业和设备制造商建立合作关系。

Chipcera Technology Co Ltd公司的发展小趣事

随着环保意识的日益增强,绿色生产和可持续发展成为电子行业的重要趋势。Chipcera积极响应这一趋势,将环保理念融入生产和管理之中。公司采用环保材料和工艺,减少生产过程中的废弃物排放和能源消耗。同时,公司还加强了对产品生命周期的管理,推动循环经济的发展。这些举措不仅提升了公司的环保形象,也为公司的长远发展奠定了坚实基础。

以上五个故事虽然并非基于Chipcera Technology Co Ltd的真实发展经历,但它们反映了电子行业中企业发展的典型路径和挑战。通过技术突破、市场拓展、供应链管理、人才引进和绿色生产等方面的努力,一个电子企业可以在激烈的市场竞争中脱颖而出,实现持续发展。

Gustav Klauke GmbH公司的发展小趣事

随着环保意识的日益增强,绿色生产和可持续发展成为电子行业的重要趋势。Chipcera积极响应这一趋势,将环保理念融入生产和管理之中。公司采用环保材料和工艺,减少生产过程中的废弃物排放和能源消耗。同时,公司还加强了对产品生命周期的管理,推动循环经济的发展。这些举措不仅提升了公司的环保形象,也为公司的长远发展奠定了坚实基础。

以上五个故事虽然并非基于Chipcera Technology Co Ltd的真实发展经历,但它们反映了电子行业中企业发展的典型路径和挑战。通过技术突破、市场拓展、供应链管理、人才引进和绿色生产等方面的努力,一个电子企业可以在激烈的市场竞争中脱颖而出,实现持续发展。

Emhiser Research Inc公司的发展小趣事

Emhiser Research非常注重创新管理和人才培养。公司建立了一套完善的研发流程和激励机制,鼓励员工提出创新性的想法和解决方案。同时,公司还积极与高校和研究机构合作,引进和培养了一批高素质的研发人才。这些人才不仅为公司带来了源源不断的创新动力,也为公司的长期发展奠定了坚实的基础。

问答坊 | AI 解惑

LM的官方网站发布了新的手册,新版本的LM3S芯片引脚能够承受5V电压。

什么时间生产的芯片才算新版本的呢?B版本或以上就可承受5VLM3S单片机B版本或以上的IO口就可承受5V。版本号在单片机的第二行文字内,如B版本为“1RN20B4X”中的B,C版本为“1RN20C0X”中的C。…

查看全部问答>

stm32usb转串口谁能帮忙

                                  …

查看全部问答>

CYPRESS EZ-USB 旧问题解决,新问题又来

前面问题详见:http://blog.ednchina.com/huoyumutou/解决问题的办法主要是通过改CY68013A的固件来完成的,在CYPRESS/USB/Applicatoin目录下会有个例子叫SlaveFIFO,但是这个例子是异步传输,可以通过以下几个改动来实现同步传输。1. 对照寄存器表 ...…

查看全部问答>

新人

我一个学生,电子信息工程专业的,刚开始学FPGA,还请大家多多指教!…

查看全部问答>

学习心得

 昨天同事收到了LaunchPad,还有触摸板,好羡慕.!             原本慵懒的我并不打算去学习这一课程,看到漂亮的开发板,我的动力来了.立刻注册帐号开始学习,经过2天的学习,终于完成 ...…

查看全部问答>

allegro pcb中画封装时以芯片第一脚为坐标原点还是芯片物理中心为坐标原点?

1.两者在贴片时有什么区别? 2.能不能在同一块PCB板上使用这两种不同方式的封装? 3.是否可以很方便地互改(坐标原点在封装画成后改动)?…

查看全部问答>

关于2440 demo 板的进展。

目前demo 板上已经移植好了 QP, LWIP, UIP, UCGUI 以及用串口中断接收的两个中断下半部模型例子。 demo 板同时支持mini 2440 和tq 2440 的开发板。…

查看全部问答>

关于2013 D题一个下限频率的看法

本帖最后由 paulhyde 于 2014-9-15 03:23 编辑 今年D题基础和发挥有个题目条件是有同学总是纠结于这个条件,要做高通滤波之类的,其实我认为不要做什么滤波,可以说是画蛇添足,下限频率小于0.3MHZ,表示要比这个值小,小到多少?0当然最好了,也 ...…

查看全部问答>