【.NET 与树莓派】数模转换
在开始之前,需要说明一对很耳熟的概念——数字信号 & 模拟信号。
这些概念的理论有些复杂,你如果相当有兴趣,可以找来有关的文献细细研究;若你不关心那是啥只想知道咋用,那就通俗但不庸俗地理解一下。
数字信号:信号值离散,不连续。显著特点它有两个值:0 和 1。CPU 最喜欢这两个值。对应到 GPIO 口上就是前面咱们说过的低电平和高电平;
模拟信号:信号内容是连续的,量化的。根据有效范围和参考对象确定的数值。这些值在某一段时间内会不断变化的。用电脑接一个大炮播放音乐,输出的就是模拟信号。咱们使用的这些电子模块,通过参考电压,将客观的物理量转化为电压。这个电压值最后可以量化为一个数值。
比如,我在门上装一个传感器,这个传感器可以感知振动的大小产生不同的值,并规定最小的振幅输出 0V 电压,振幅 XXX (最大值)输出 5V 电压。这样一来,你就知道每天从你家门前经过的人里面有多少人会踢你的门,踢的力度有多大。
像我们常见的旋扭,音响上面调音量的那个,就可以你旋转不同的角度,输出不同的电压值,然后你可以规定一个范围:0 - 1023,即10位精度(二进制位),0表示音量最小,1023表示音量最大。如果旋扭旋到中间的某个地方,可以根据模拟量到计算音量的百分比。我要是旋到 511 左右,那就把音量控制为 50%。
现在,你可以猜猜,树莓派的 GPIO 引脚通信时用的是上述的哪一种信号?复习一下,高考必考的哟,咱们在操作引脚时,是不是经常控制它输出高电平、低电平;或者读入高电平、低电平?嗯,这就是了,它就认识两个值——0、1。看来,树莓派的通信引脚处理的是数字信号。
很遗憾的是,树莓派没有硬件层集成的模拟信号引脚。像 Arduino 板子上的 A0 - A5,就是模拟引脚,可以读出输入的模拟信号。咱们的大草莓并没有这类引脚。当然你会想到,我能不能用 Arduino 来读模拟信号,再传给树莓派就行了。思想很正确,肯定可以这样做的。传输方式你可以选用最简单的串口通信;也可以用 I2C、SPI 等协议来传输;或者用带Wi-fi的 Arduino 板子(或加装无线模块)用无线网来传,协议可以自由选——TCP、UDP,就是Socket编程呗……总之,只要你会玩,咋玩都行。
不过,话又说回来,如果你手头上没有 Arduino 板子,就为了读个模拟量去买块开发板,这也不划算。所以呢,最“秀”的解决方案是买个数模转换模块。
树莓派官方系统有原生驱动,支持一些数模转换模块(这种情况下也可以买个带数模转换的扩展板,价格比较吓人),比如 ADS1115,这模块不错,老周也想买一块,但在某宝转了一圈,既找到好用的现成模块,单卖芯片的倒是很多,但不方便使用。如果咱们只是学习或实验,或者做的东东非工业级的,或者对精度要求不高的话,找个 8 位的数模转换模块就够了。
某宝上既便宜又容易找到的就是这种。
不同厂家生产的模块,外形有点不同,芯片是 PCF8591。还是老规矩,哪个便宜买哪个。
这个模块是 8 位的,也就是说,你读到的数值范围在 0 到 255,正好是一个字节。说实,老周觉得这精度算是可以的,想想咱们平时调整音量也就是 0% - 100%,只有 100 个值,而用这个模块能得到 256 个值,非特殊需求够用了。
这个模块有两边引脚,看个正面的图。
焊接的引脚是弯的,会挡住PCB板上印的丝印文字,但没啥关系。
先看左边,第一个是 AOUT 这个是输出的,根据模拟量输出不同的电压。我们重点关注后面四个,这四个都是模拟输入。也就是说,PCF8591模块有四个通道可以用,可以读到来自四个设备的信号(前提是这四路是相互独立的,而不是差分值)。
右边一排就是标准的 I2C 引脚,SCL接时钟线,SDA是数据线,GND接负,VCC接 5V。
有意思的是,这个模块它集成了三个传感器:热敏电阻、光敏电阻和电位器。热敏电阻感知外部温度输出模拟量,光敏电阻感知外部光照输出模拟量,而电位器则是通过螺丝刀旋转来得到不同的电阻值,并转化为模拟量。
这三个传感器默认是与AIN0 - AIN4相连的(只用到三个通道,有一个是空的,这个你得问卖家,到底哪个是空的)。也就是说,你在不连接其他器件时,PCF8591也能读到数值,数值来源于上述几个集成的传感器。
但在本文中,老周是想让大草莓读取压电陶瓷输出的模拟信号,并且压电陶瓷模块是接在 AIN0 引脚上。那么,这就要解决一个问题:如何让PCF8591模块不读取集成的那三个传感器的信号而是读我们自己连接的器件信号?请注意看,这个模块上有三个短路帽,下图中绿色标注的地方。
怎么做呢,很简单,直接拔掉,不用工具的,徒手就能拔,就像拔牙一样拔。 这三个家伙就是和集成的三个传感器连接的,拔掉短路帽,就不通了。这样就能读你自己连接的器件了。
====================================================
下面说说要编程时怎么用 PCF8591 模块。
1、从机地址:0x48。
2、协议很好办,就发送一个字节就行了,称为控制字节(Control Byte)。这个字节的每个二进制位可以进行参数设置。
b7 b6 b5 b4 b3 b2 b1 b0
b7 位固定为 0,不用管,就给它 0 就行。
b6 指定 AOUT 引脚是否启用,即是否启用模拟输出。0 不启用;1 启用。我们这里只是用来读数据,不启用,给它0即可。
b5 和 b4 用来选择四个通道的编制方式。
00:每个通道独立。其实本文的示例就用这种方式,一通道接一设备就行。
01:三路差值,两个引脚的值相减得到的结果存入一个通道。这种情况下可能产生负值,所以 8 位的范围是 -128 到 127,这种情况要用sbyte 类型来表示。
channel 0 = AIN0 - AIN1
channel 1 = AIN1 - AIN2
channel 2 = AIN2 - AIN3
故差分方式仅存储三个通道的数值。
10:混合方式。AIN0 引脚 读到的值存入通道0,AIN1 引脚处的数据存到通道1;最后 AIN2 - AIN3 的差分结果存到通道2。只用到三个通道。
11:两路差分。
channel 0 = AIN0 - AIN1
channel 1 = AIN2 - AIN3
b3 位为固定值——0。
b2 位配置通道地址是否自增。即读取完 channel0,指针会自动跳到 channel 1。为了读取更灵活,咱们不启用,设置为 0。
b1 和 b0 位,指定要访问的通道。
00 -> channel 0
01 -> channel 1
10 -> channel 2
11 -> channel 3
所以,根据本文示例的情形,b6 为 0,b5、b4 为 0,b2 为 0,最后得到各个通道的地址为:
const byte CN1 = 0x00; const byte CN2 = 0x01; const byte CN3 = 0x02; const byte CN4 = 0x03;
示例用到压电陶瓷模块。
注意,陶瓷片和PCB板买回来一般是没接线的,咱们自己接也很好办,用螺丝刀把接线柱的螺丝拧松,然后把线捅进去,再拧紧螺丝即可。注意这玩意儿是分正负的,红色的线接“INPUT”,黑色的线接“GND”。接好之后就是这样。
压电陶瓷模块有三个引脚,- 接GND,+ 接5V,S 输出信号,接 PCF8591 的 AIN0 引脚。
接线的时候,最好把树莓派的 5V 和 GND 引出到面包板。面包板一般有两排供电专用的孔。
毕竟大草莓上只有两个 5V 引脚,如果你还要接个散热风扇,就不怎么够用了,所以引出来可以接很多器件;另一方面,把 5V 和 GND 引出来,可以方便让 PCF8591 模块和压电陶瓷模块共地,这样它们对于相对 0V 有个参考值,也能使模拟信号更稳定。
=========================================================
最后一步,就是写程序了。
先声明一些必备常量。
// 从机地址 const int ADDR = 0x48; // 以下是读各个转换通道的控制字节 /* MSB LSB 0 X X X 0 X X X | | | | |__| | | | | 这两位用来选择通道 | | | | 00 - AIN0 01 - AIN1 | | | | 10 - AIN2 11 - AIN3 | | | |--该位指定通道地址是否自动增长,1启用,0不启用 | |__| | 这两位设置通道对模拟信号的编制方式 | 00 - 每个通道独立,一一对应着,即单端信号 | AIN0 -> channel 0 | AIN1 -> channel 1 | AIN2 -> channel 2 | AIN3 -> channel 3 | 01 - 三路差分信号 | AIN0 - AIN1 -> channel 0 | AIN1 - AIN2 -> channel 1 | AIN2 - AIN3 -> channel 2 | 四引脚共地 | 10 - 单端信号 + 差分信号 | AIN0 -> channel 0 | AIN1 -> channel 1 | AIN2 - AIN3 -> channel 2 | 前两个是单端信号 | 11 - 两路差分信号 | AIN0 - AIN1 -> channel 0 | AIN2 - AIN3 -> channel 1 | AIN1,AIN3 不共地 |--是否让模块开启模拟信号输出,即数字信号转模拟信号(AOUT引脚) */ const byte CN1 = 0x00; const byte CN2 = 0x01; const byte CN3 = 0x02; const byte CN4 = 0x03;
接着,连接 I2C 设备。
I2cConnectionSettings cst = new(1, ADDR); I2cDevice device = I2cDevice.Create(cst);
这里压电陶瓷模块只用了 PCF8591的 AIN0 引脚,所以只用到通道0。
device.WriteByte(CN1);
从始至终我们只用到一个通道,所以控制字节我们发送一次就可以了,除非你要重新选择其他通道,或修改参数。
然后 PCF8591 模块会不断把读到的值存到通道的寄存器中,咱们只需要不断的 read 就行了。
while (looping) { readval = device.ReadByte(); // 计算要输出的字符数 int outputs = readval * 100 / 255 * 80 / 100; // 0 不输出 if (outputs == 0) { continue; } for (int x = 0; x < outputs; x++) { Write("▮"); } …… }
转换精度是 8 位,正好读一个字节就够。
上面代码的意思:根据读到的模拟量,算出百分比,把值乘以 100 再除以 255,防止整数运算后舍入为0。比如,要是计算结果为 0.12,就会变成0了。80表示若值是 255 时(100%)在控制台总共输出 80 块黑色砖头(▮),如果是 50% 就输出 40 块砖头。由于前面乘以 100 放大了数值,所以后面要除以100来“中和”一下。当然,这样算有点乱,你觉得这样不好,可以转成浮点数来算,再转回整数就行了。
这个示例的原理,就是压电陶瓷在受到不同的外力时电压会改变,因此输出不同的模拟信号,当咱们用东西敲打陶瓷片(圆圆的像块饼干的那个),力度不同就会看到屏幕上输出不数量的砖头。
一场浩大的打击乐器表演即将开始。
是不是很有魔性?
最后,把完整代码贴一下,老周就不上传 .zip 了,担心以后博客空间不够用。
using System; using static System.Console; using static System.Threading.Thread; using System.Device.I2c; namespace dacapp { class Program { // 从机地址 const int ADDR = 0x48; // 以下是读各个转换通道的控制字节 /* MSB LSB 0 X X X 0 X X X | | | | |__| | | | | 这两位用来选择通道 | | | | 00 - AIN0 01 - AIN1 | | | | 10 - AIN2 11 - AIN3 | | | |--该位指定通道地址是否自动增长,1启用,0不启用 | |__| | 这两位设置通道对模拟信号的编制方式 | 00 - 每个通道独立,一一对应着,即单端信号 | AIN0 -> channel 0 | AIN1 -> channel 1 | AIN2 -> channel 2 | AIN3 -> channel 3 | 01 - 三路差分信号 | AIN0 - AIN1 -> channel 0 | AIN1 - AIN2 -> channel 1 | AIN2 - AIN3 -> channel 2 | 四引脚共地 | 10 - 单端信号 + 差分信号 | AIN0 -> channel 0 | AIN1 -> channel 1 | AIN2 - AIN3 -> channel 2 | 前两个是单端信号 | 11 - 两路差分信号 | AIN0 - AIN1 -> channel 0 | AIN2 - AIN3 -> channel 1 | AIN1,AIN3 不共地 |--是否让模块开启模拟信号输出,即数字信号转模拟信号(AOUT引脚) */ const byte CN1 = 0x00; const byte CN2 = 0x01; const byte CN3 = 0x02; const byte CN4 = 0x03; static void Main(string[] args) { // 初始化i2c连接 I2cConnectionSettings cst = new(1, ADDR); I2cDevice device = I2cDevice.Create(cst); // 变量标志程序是否应继续循环 bool looping = true; // 当收到 Ctrl + C 时释放资源 CancelKeyPress += (_, _) => looping = false; // 选择读通道1的数据 device.WriteByte(CN1); byte readval = default; //读到的数据 while (looping) { readval = device.ReadByte(); // 计算要输出的字符数 int outputs = readval * 100 / 255 * 80 / 100; // 0 不输出 if (outputs == 0) { continue; } for (int x = 0; x < outputs; x++) { Write("▮"); } Write('\n'); Sleep(500); } } } }
====================================================================
【题外话】
有同学可能会有疑问:树莓派我买 4B 还是 400 ?这个嘛,你得知道,400 是把主板装在一个键盘里面的,40 pin 针脚没少,但少了个 USB 接口。体积肯定较大,如果你还是喜欢手巴掌以内的尺寸,还是买 4B 吧,弄个外壳携带也方便,尽管CPU不能和英特尔干,但出差时候作为一体机耍耍还不错,它有 GPIO 针脚,必要时还可以帮客户调一调硬件。
- 要是你不带出去,把它做成监控主机(买个摄像头模块就完事,至于多少钱的就看你的银行卡容量了)也挺爽;
- 刷个 KODI 当电视盒子;
- 加个手柄开《极品破车》(这游戏不知道有没Linux版);
- 发挥蓝牙作用 + SSD可以做个床头小唱机、随身听啥的;+ 光驱可以弄成影碟机;
- 做个无线投影仪也可以;
- 买些可编程的灯带,把你家变成K厅也行;
- 弄个刷卡门锁也行(停电了就麻烦);
- 可以用来做直播(直播赤脚踩刀锋);
- 不嫌主板太大的话,搞个红外人体识别模块做成感应楼梯灯(住小区的话就别弄了,邻居一伸手就把你的草莓吃掉);
- 老周买了个人体称重模块,自己弄了个体重测量仪,没事可以看看现在自己有多胖(国药的秤只能给小学生用);
- 买个 PH 玻璃探头,可以检测自来水酸碱性(就是用到本文的数模转换,TDS检测头原理一样),这个挺贵的;
- 把你吃灰的硬盘全拿出来,建个家庭网盘。嗯,常说的NAS。不过,无线路由的WIFI信号好像不太给力。
- 超声波洗鱼缸。声波发生器不好买,老周是借了一个 9V 的模块(人家工厂里面用的)玩了两下,没做成,根本洗不干净。电压 9 V,要独立供电。也不知道他们厂用这些……估计是驱蚊用的。
- 紫外线强度实时监测。模块某宝上有,但是把大草莓挂在外面晒,有点怕。
……
反正,只要你敢想,并付诸实践,什么 DIY 方式都能实现。