• 51单片机外设篇:ADC


    概念引入

    ADC:analog digital converter,AD转换,模数转换(也就是模拟转数字)

    CPU本身是数字的,而外部世界变量(如电压、温度、高度、压力···)都是模拟的,所以需要用CPU来处理这些外部的模拟变量的时候就需要做AD转换。

    为什么需要ADC?为了用数字技术来处理外部的模拟物理量。

    关于模拟量和数字量
    模拟的就是连续的,现实生活当中的时间、电压、高度等都是模拟的(连续分布的,划分的话可以无限的更小划分)。模拟量反映在数学里面就是无限小数位(从0到1之间有无数个数)

    数字的就是离散的,离线的就是不连续的。这种离散处理实际上是从数学上对现实中的模拟量的一种有限精度的描述。数字化就是离散化,就是把连续分布的模拟量按照一定精度进行取点(采样)变成有限多个不连续分布的数字值,就叫数字量。

    数字化的意义就在于可以用(离散)数学来简化描述模拟量,这东西是计算机技术的基础。


    计算机处理参量的时候都是数字化的,计算机需要数字化的值来参与运算。如果系统输入参数中有模拟量,就需要外加AD转换器将模拟量转成数字量再给计算机。

    有AD自然就有DA
    AD是analog to digital,DA自然就是digital to analog,数字转模拟。
    纯粹用cpu是不可能实现数字转模拟,因为cpu本身就是数字的。使用一些(具有一些积分或微分效果的)物理器件就可实现数字转模拟。
    数字转模拟的作用。譬如可以用来做波形发生器。


    ADC的主要相关概念:


    量程(模拟量输入范围)
    AD转换器是一个电子器件,所以他只能输入电压信号。其他种类的模拟信号要先经过传感器(Sensor)的转换变成模拟的电压信号然后才能给AD。
    AD输入端的模拟电压要求有一个范围,一般是0~3.3V或0~5V或者是0~12V等等。模拟电压的范围是AD芯片本身的一个参数。实际工作时给AD的电压信号不能超过这个电压范围。

    精度(分辨率resolution)
    AD转换输出的数字值是有一定的位数的(譬如说10位,意思就是输出的数字值是用10个二进制位来表示的,这种就叫10位AD)。这个位数就表示了转换精度。
    10位AD就相当于把整个范围分成了1024个格子,每个格子之间的间隔就是电压的表示精度。加入AD芯片的量程是0~3.3V,则每个格子代表的电压值是3.3V/1024=0.0032265V。如果此时AD转换后得到的数字量是447,则这个数字量代表的模拟值是:447×0.0032265V=1.44V。


    AD的位数越多,则每个格子表示的电压值越小,将来算出来的模拟电压值就越精确。


    AD的模拟量程一样的情况下,AD精度位数越多精度越高,测出来的值越准。但是如果AD的量程不一样。譬如2个AD,A的量程是0~50V,B的量程是0~0.5V,A是12位的,B是10位的,可能B的精度比A的还要高。

    (A的精度:50/1024=0.04883,B的精度:0.5/4096=0.000122)

    转换速率(MSPS与conventor clock的不同)
    首先要明白:AD芯片进行AD转换是要耗费时间的。这个时间需要多久,不同的芯片是不一样的,同一颗芯片在配置不一样(譬如说精度配置为10位时时间比精度配置为12位时要小,譬如说有些AD可以配转换时钟,时钟频率高则转换时间短)时转换时间也不一样。
    详细的需要时间可以参考数据手册。一般数据手册中描述转换速率用的单位是MSPS(第一个M是兆,S是sample,就是采样;PS就是per second,总的意思就是兆样本每秒,每秒种转出来多少M个数字值)


    AD工作都需要一个时钟,这个时钟有一个范围,我们实际给他配置时不要超出这个范围就可以了。AD转换是在这个时钟下进行的,时钟的频率控制着AD转换的速率。注意:时钟频率和MSPS不是一回事,只是成正比不是完全相等。譬如S5PV210中的AD转换器,MSPS = 时钟频率/5。

    通道数
    AD芯片有多少路analog input通道,代表了将来可以同时进行多少路模拟信号的输入。

    查看原理图

    其中,ET2046是一个触摸屏的控制器芯片,因为触摸屏实质上就是AD转换,所以用该控制器来演示ADC。

    关于芯片的具体内容参考数据手册:ET2046 - 百度文库

    我们这里仅记录重点内容。

    1、该芯片采用的是SPI总线协议,重点关注以下4个引脚,DIN,DOUT,CS,DCLK,正好是SPI协议的四根引脚,两根数据线,一根时钟线,一根片选线。我们将其分别接到如下引脚。CLK接P1.0,CS接P1.1,DI接P1.2,DQ接P1.3。

    2、有三个模拟量输入。AIN0靠滑动变阻器控制电压变化,AIN1靠热敏电阻NTC控制电压变化,AIN2靠光敏电阻GR1控制电压变化。

    ET2046控制字 

    控制字(加在DIN端口)提供ET2046的以下信息:开始转换标志位、地址、ADC的精度、配置方式和Power-Down模式的选择,如下所示:

    bit7:起始位,必须保持为高电平来表示控制字开始传输。在未侦测到起始位时会忽略DIN引脚上的所有输入。

    bit6-4:决定采样的是哪一路(AIN0、AIN1、AIN2、AIN3)

    AIN0:001/011    X+
    AIN1:101           Y+
    AIN2:010          VBAT
    AIN3:110          AUX

    bit3:设置采样位数。0表示12bit,1表示8bit,一般都用12bit。

    bit2:为1表示用单端模式,为0表示差分模式。此处用单端模式。

    bit1-0:power down模式(掉电工作模式,属于低功耗模式)使能,00表示使能。

    总结来看:

    读AIN0时写入:0b10010100    = 0x94
    读AIN1时写入:0b11010100    = 0xD4    
    读AIN2时写入:0b10100100    = 0xA4
    读AIN3时写入:0b11100100    = 0xE4

    读写时序

     属于SPI总线协议,但又不完全是。

    1、上升沿写入,下降沿读出;

    2、读写都是高位在前;

    3、BUSY信号是为了检测AD是否在忙,这个忙的时段,就是AD正在转换的时段。但是一般不用管,因为我们只用在写完控制字后延时一段时间即可(比如1us),不必为了这段检测,多花费一个IO口。

    4、所以,在写完控制字后,有一个稍微长一点的延时,让AD完成转换,然后再读取。

    5、因为控制字里一般选择12bit精度,所以读出的数据是12位二进制数,需要用uint来接收。

    数值计算

    float = 接收到的12位数,转成十进制数,然后除以2^12。

    得到的比例,然后乘以量程。也就是最后得到的电压值。

    留个思考题:如果将数值以字符的形式通过串口显示出来?

    代码实现

    1. #include "ET2046.h"
    2. uint Read_AD_Data(uchar cmd)
    3. {
    4. uchar i;
    5. uint AD_Value = 0; // 局部变量的初始化非常重要
    6. CLK = 0;
    7. CS = 0;
    8. for(i=0; i<8; i++)
    9. {
    10. DIN = cmd >> 7; //放置最高位
    11. cmd <<= 1;
    12. CLK = 0; //上升沿放置数据
    13. CLK = 1;
    14. }
    15. for(i=6; i>0; i--); //延时等待转换结果
    16. CLK = 1; //发送一个时钟周期,清除BUSY
    17. _nop_();
    18. _nop_();
    19. CLK = 0;
    20. _nop_();
    21. _nop_();
    22. for(i=0; i<12; i++) //接收12位数据
    23. {
    24. AD_Value <<= 1;
    25. CLK = 1;
    26. CLK = 0;
    27. AD_Value |= DOUT;
    28. }
    29. CS = 1;
    30. return AD_Value;
    31. }

    main.c

    1. #include "ET2046.h"
    2. #include "uart.h"
    3. #define CMD_READ_AIN0 0x94 // 滑动变阻器
    4. #define CMD_READ_AIN1 0xD4 // NTC
    5. #define CMD_READ_AIN2 0xA4 // GR1
    6. #define CMD_READ_AIN3 0xE4 // 外部输入的电压值
    7. // AD value是12bit的,分2拨发出去
    8. void uart_send_advalue(uint val)
    9. {
    10. uart_send_byte((val >> 8) & 0xff); // 高8位
    11. uart_send_byte(val & 0xff); // 低8位
    12. uart_send_byte(0);
    13. }
    14. void delay1s(void) //误差 0us
    15. {
    16. unsigned char a,b,c;
    17. for(c=167;c>0;c--)
    18. for(b=171;b>0;b--)
    19. for(a=16;a>0;a--);
    20. _nop_(); //if Keil,require use intrins.h
    21. }
    22. // 思路:用51单片机来计算,直接算出最终的电压值,然后通过串口发送显示
    23. // 第1步:先去算出电压值(val/4096)*5000mV =1.22*val(mV)
    24. // 第2步:串口发送出去显示。显示方法1,用二进制发送出去显示
    25. // 方法2:用文本方式去显示.把算出来要发送的数值,转成对应的ASCII码发送
    26. // 给串口去显示
    27. // 以文本方式直接打印电压值
    28. void uart_send_voltage(uint val)
    29. {
    30. // 先将AD值val换算成mV为单位的电压值
    31. float index = 1.22;
    32. float voltage = index * val;
    33. uint vol_display = (uint)voltage;
    34. uart_send_text2(vol_display);
    35. }
    36. void main(void)
    37. {
    38. uint val = 0;
    39. uart_init();
    40. // uart_send_voltage(3241);
    41. // while (1);
    42. while (1)
    43. {
    44. val = Read_AD_Data(CMD_READ_AIN0); // 滑动变阻器,测试ok
    45. // val = Read_AD_Data(CMD_READ_AIN1); // NTC,测试ok
    46. // val = Read_AD_Data(CMD_READ_AIN2); // 光敏电阻,测试ok
    47. //uart_send_advalue(val);
    48. uart_send_voltage(val);
    49. delay1s();
    50. }
    51. }

    uart.c

    1. #include "uart.h"
    2. // 串口设置为: 波特率9600、数据位8、停止位1、奇偶校验无
    3. // 使用的晶振是11.0592MHz的,注意12MHz和24MHz的不行
    4. void uart_init(void)
    5. {
    6. // 波特率9600
    7. SCON = 0x50; // 串口工作在模式1(8位串口)、允许接收
    8. PCON = 0x00; // 波特率不加倍
    9. // 通信波特率相关的设置
    10. TMOD = 0x20; // 设置T1为模式2
    11. TH1 = 253;
    12. TL1 = 253; // 8位自动重装,意思就是TH1用完了之后下一个周期TL1会
    13. // 自动重装到TH1去
    14. TR1 = 1; // 开启T1让它开始工作
    15. // ES = 1;
    16. // EA = 1;
    17. }
    18. // 通过串口发送1个字节出去
    19. void uart_send_byte(unsigned char c)
    20. {
    21. // 第1步,发送一个字节
    22. SBUF = c;
    23. // 第2步,先确认串口发送部分没有在忙
    24. while (!TI);
    25. // 第3步,软件复位TI标志位
    26. TI = 0;
    27. }
    28. // 以文本方式发送c过去,意思就是要串口助手用文本方式来查看,看到的是
    29. // 这个数字本身
    30. void uart_send_text(unsigned char c)
    31. {
    32. // 思路就是把c以十进制方式显示的几个数字,挨个变成文本发送出去
    33. unsigned char i;
    34. // 先计算得出c的最高位,然后发出去
    35. i = c / 100;
    36. uart_send_byte(i + 48);
    37. // 然后计算次高位
    38. c = c % 100;
    39. i = c / 10;
    40. uart_send_byte(i + 48);
    41. // 然后计算个位
    42. c = c % 10;
    43. i = c;
    44. uart_send_byte(i + 48);
    45. // 发送一个换行
    46. uart_send_byte('\r');
    47. uart_send_byte('\n');
    48. }
    49. // 因为我们知道电压值不会超过5000,所以只考虑显示1万以内的数值
    50. // 这种方式限制能限制的最大值是9999
    51. void uart_send_text2(unsigned int c)
    52. {
    53. // 思路就是把c以十进制方式显示的几个数字,挨个变成文本发送出去
    54. unsigned char i;
    55. i = c / 1000;
    56. uart_send_byte(i + '0');
    57. uart_send_byte('.');
    58. // 先计算得出c的最高位,然后发出去
    59. c = c % 1000;
    60. i = c / 100;
    61. uart_send_byte(i + 48);
    62. // 然后计算次高位
    63. c = c % 100;
    64. i = c / 10;
    65. uart_send_byte(i + 48);
    66. // 然后计算个位
    67. c = c % 10;
    68. i = c;
    69. uart_send_byte(i + 48);
    70. uart_send_byte('V');
    71. // 发送一个换行
    72. uart_send_byte('\r');
    73. uart_send_byte('\n');
    74. }
  • 相关阅读:
    【JavaWeb】Servlet属性设置
    树莓派4B已安装opencv4.6.0但是用thonny编译调用不了树莓派原装摄像头
    Matlab | TCP通信_项目练习
    麒麟 ZYJ 服务器软件适配 参考示例
    优秀智慧园区案例 - 三亚市崖州湾科技城智慧园区,先进智慧园区建设方案经验
    Java项目的程序里为什么老用注解?注解有哪些作用
    JVM下篇(三、JVM监控及诊断工具-GUI篇)
    【算法-数组1】二分查找 和 移除元素
    Linux网络编程(三)
    Java流的体系结构(二)
  • 原文地址:https://blog.csdn.net/qq_28576837/article/details/125993028