概述

通过前面课程的学习,我们知道raspberry Pi可以做很多事情,能处理各种数字传感器,但是美中不足的是Raspberry Pi不能直接处理模拟信号,因为Pi没有ADC(analog to digital converters ),这样会限制输出模拟信号的传感器无法在Pi上使用,那如何解决这个问题呢?本课我们将一起探索如何用Pi读取模拟信号。

所需物料

1 * Raspberry Pi

1 * Breadboard

1 * MCP3008

Several jumper wires

工作原理

其实要解决上面的问题并不难,只需要在Pi上扩展一个模数转换器(ADC)就可以了,模数转化器有很多类型。在本课中我们使用Micochip公司的MCP3008,如果你对输出通道数量没有要求,MCP3004也是可以的。MCP3008一个10位8通道的ADC,MCP3004是10位4通道ADC,除了通道数量不一样,其他特性都相同,都是SPI(Serial Peripheral Interface Bus)接口,关于SPI总线更多信息请看这里,工作电压为2.7-5.5 VDC,关于MCP3004/8更多特性请参考芯片数据手册:https://osoyoo.com/driver/MCP3008_datasheet.pdf

MCP3008

MCP3008可以将模拟信号转换成数字信号,这样Pi就能够间接处理模拟信号了,在本课中,用pi的3.3V和GND分别作为MCP3008的模拟通道1和通道2的输入,MCP3008将这个模拟输入信号转换成数字量,Pi通过SPI读取这个数字量,通过计算将其实际电压值输出

MCP3008是SPI接口,所以将其与Raspberry Pi的硬件SPI相应GPIO口连接在一起。图中的NC(Not Connected)表示悬空,啥也不接。

实物连线图

软件

对于C语言用户,请看下面

1 ) MCP3008需要SPI时序来驱动,我们用的是Raspberry Pi的硬件SPI,但是对于有些Raspbian镜像在内核中默认没有加载有些模块,如SPI、IIC等,所以在进行编程之前需要检查你的Raspbian是否已经加载了SPI,如果没有需要手动加载,具体方法前看看面。

用编辑器打开‘/etc/modprobe.d/raspi-blacklist.conf’

sudo  nano  ‘/etc/modprobe.d/raspi-blacklist.conf’

如果打开什么东西也没有,说明你的Raspbian已经加载了相关模块,请跳过此步;如果看到这样的内容

#blacklist spi-bcm2708
#blacklist i2c-bcm2708

说明你的Raspbian没有加载SPI、IIC设备,你需要将这两句话前面的#去掉

打开SPI接口

sudo  nano /boot/config.txt

打开config.txt文件后,找到dtparam=spi=on(或者dtparam=spi=off),将其改成图示样子

按键盘上的Ctrl +X根据提示输入Y保存退出,让后在命令行输入reboot是修改生效。

2)  在/home/pi目录下新建一个.c源文件,名字随意(你开心就好)

cd  ~

sudo  nano  readmcp300x.c

2) 编码

向新建的文件中输入一下代码

#include <unistd.h>
#include <stdint.h>
#include <string.h>
#include <errno.h>
#include <wiringPi.h>
#include <stdio.h>
#include <stdlib.h>
#include <wiringPiSPI.h>
 
#define TRUE                (1==1)
#define FALSE               (!TRUE)
#define CHAN_CONFIG_SINGLE  8
#define CHAN_CONFIG_DIFF    0
 
static int myFd ;
 
char *usage = "Usage: mcp3008 all|analogChannel[1-8] [-l] [-ce1] [-d]";
// -l   = load SPI driver,  default: do not load
// -ce1  = spi analogChannel 1, default:  0
// -d   = differential analogChannel input, default: single ended
 
void loadSpiDriver()
{
    if (system("gpio load spi") == -1)
    {
        fprintf (stderr, "Can't load the SPI driver: %s\n", strerror (errno)) ;
        exit (EXIT_FAILURE) ;
    }
}
 
void spiSetup (int spiChannel)
{
    if ((myFd = wiringPiSPISetup (spiChannel, 10000)) < 0)
    {
        fprintf (stderr, "Can't open the SPI bus: %s\n", strerror (errno)) ;
        exit (EXIT_FAILURE) ;
    }
}
 
int myAnalogRead(int spiChannel,int channelConfig,int analogChannel)
{
    if(analogChannel<0 || analogChannel>7)
        return -1;
    unsigned char buffer[3] = {1}; // start bit
    buffer[1] = (channelConfig+analogChannel) << 4;
    wiringPiSPIDataRW(spiChannel, buffer, 3);
    return ( (buffer[1] & 3 ) << 8 ) + buffer[2]; // get last 10 bits
}

void print_info()
{
    printf("\n");
    printf("|*****************************************|\n");
    printf("|    Read MCP3008(3004) ADC value         |\n");
    printf("|    ------------------------------       |\n");
    printf("|       | ADC |           | Pi  |         |\n");
    printf("|       |-----|-----------|-----|         |\n");
    printf("|       | CS  | connect to| CE0 |         |\n");
    printf("|       | Din | connect to| MOSI|         |\n");
    printf("|       | Dout| connect to| MISO|         |\n");
    printf("|       | CLK | connect to| SCLK|         |\n");
    printf("|       | CH0 | connect to| 3.3V|         |\n");
    printf("|       | CH1 | connect to| GND |         |\n");
    printf("|                                   OSOYOO|\n");
    printf("|*****************************************|\n");
    printf("\n");

} 

int main (int argc, char *argv [])
{
    int loadSpi=FALSE;
    int analogChannel=0;
    int spiChannel=0;
    int channelConfig=CHAN_CONFIG_SINGLE;
    if (argc < 2)
    {
        fprintf (stderr, "%s\n", usage) ;
        return 1 ;
    }
    if((strcasecmp (argv [1], "all") == 0) )
        argv[1] = "0";
    if ( (sscanf (argv[1], "%i", &analogChannel)!=1) || analogChannel < 0 || analogChannel > 8 )
    {
        printf ("%s\n",  usage) ;
        return 1 ;
    }
    int i;
    for(i=2; i<argc; i++)
    {
        if (strcasecmp (argv [i], "-l") == 0 || strcasecmp (argv [i], "-load") == 0)
            loadSpi=TRUE;
        else if (strcasecmp (argv [i], "-ce1") == 0)
            spiChannel=1;
        else if (strcasecmp (argv [i], "-d") == 0 || strcasecmp (argv [i], "-diff") == 0)
            channelConfig=CHAN_CONFIG_DIFF;
    }
    //
    if(loadSpi==TRUE)
        loadSpiDriver();
    wiringPiSetup () ;
    spiSetup(spiChannel);
    print_info();
    //
    if(analogChannel>0)
    {
        printf("MCP3008(CE%d,%s): analogChannel %d = %d\n",spiChannel,(channelConfig==CHAN_CONFIG_SINGLE)
               ?"single-ended":"differential",analogChannel,myAnalogRead(spiChannel,channelConfig,analogChannel-1));
    }
    else
    {
        for(i=0; i<8; i++)
        {
            printf("MCP3008(CE%d,%s): analogChannel %d = %d\n",spiChannel,(channelConfig==CHAN_CONFIG_SINGLE)
                   ?"single-ended":"differential",i+1,myAnalogRead(spiChannel,channelConfig,i));
        }
    }
    close (myFd) ;
    return 0;
}

按键盘上的Ctrl +X根据提示输入Y保存退出。

完整的程序源码,可以在命令行运行一下命令获取

sudo  wget  https://osoyoo.com/driver/pi3_start_learning_kit_lesson_10/readmcp3008.c

在程序开头先包含了程序运行需要的头文件,其中有两个是驱动MCP3008必须的头文件,一个是wiringPi,一个是wiringPiSPI。对于前者在之前的课程中都有用到,就不在赘述;后者是针对树莓派板载SPI开发的一个函数库,利用这个库可以更加容易使用Raspberry Pi板载SPI功能,关于wiringPiSPI更多信息请看这里

SPI Library

#define CHAN_CONFIG_SINGLE  8
#define CHAN_CONFIG_DIFF    0

因为MCP3008支持单端信号输入和(single-ended mode)和差分输入(differential input mode),本课中我采用单端输入模式,CHAN_CONFIG_SINGLE变量表示相应输入端(CHx)配置为单端输出模式(8表示通道0为单端输入模式),CHAN_CONFIG_DIFF表示相应输入端(CHx)配置为差分输入模式(0意思是说通道0为差分输入模式),依据数据手册可以计算每个通道在不同输入模式下的值

static int myFd ;

定义了一个文件描述符( file-descriptor),用于接收wiringPiSPISetup函数的返回值

char *usage = "Usage: mcp3008 all|analogChannel[1-8] [-l] [-ce1] [-d]";
// -l   = load SPI driver,  default: do not load
// -ce1  = spi analogChannel 1, default:  0
// -d   = differential analogChannel input, default: single ended

usage指针里面存放了这个程序的用法,如果在运行程序时候输入参数出错,屏幕会打印出usage变量,提示用户正确输入

void spiSetup (int spiChannel)
{
    if ((myFd = wiringPiSPISetup (spiChannel, 10000)) < 0)
    {
        fprintf (stderr, "Can't open the SPI bus: %s\n", strerror (errno)) ;
        exit (EXIT_FAILURE) ;
    }
}

这是一个自定义函数,在这个函数里面调用了wiringPiSPI库里面的wiringPiSPISetup函数,初始化spiChannel号SPI,并SPI通信速率设置成了10KHz,其中spiChannel表示Raspberry Pi第几个SPI,Raspberry Pi一共有2个硬件SPI,它们共用SCLK、MOSI、MISO三条线,通过CE0和CE1区分SPI0和SPI1,在本课中我们将MCP3008的片选脚(Chip Select/Shutdown Input)接到Pi的CE0上,即我们使用的是Pi的SPI0.如果wiringPiSPISetup返回值小于0,说明SPI初始化出错,并在屏幕上打印出错信息。

int myAnalogRead(int spiChannel,int channelConfig,int analogChannel)
{
    if(analogChannel<0 || analogChannel>7)
        return -1;
    unsigned char buffer[3] = {1}; // start bit
    buffer[1] = (channelConfig+analogChannel) << 4;
    wiringPiSPIDataRW(spiChannel, buffer, 3);
    return ( (buffer[1] & 3 ) << 8 ) + buffer[2]; // get last 10 bits
}

这个函数用于读取MCP3008数据,因为MCP3008只有8个输入端,如果参数analogChannel不在0-7范围内,返回-1;结合MCP3008数据手册知道第一个子节和第二个子节的高6位是无效的,有效数据为是第二个子节的低2位和第三个子节,一共10位。

3) 编译

gcc  -Wall -o readmcp3008   readmcp3008 .c  -iwiringPi

4) 执行程序

a)在运行程序之前请用gpio  readall的命令检查MOSI、MISO、SCLK(B10、B9、B11)这几个口是否工作于ALT0模式(alternative functions),如果这几个口处于ALT0模式,则可以跳到b;如若不是请用下面的shell命令将他们变成ALT0模式

gpio  -g  mode  9    alt0

gpio  -g  mode  10   alt0

gpio  -g  mode   11   alt0

如果你想查看8个通道的值,运行下面的命令:

./readmcp3008  all

如果你只想查看指定通道的值,例如通道1,运行下面的命令

./readmcp3008  1

5) 最终结果

正确输入参数,运行上面的程序,屏幕上会打印出MCP3008主要引脚与Pi的连接关系,也会打印出你想查看的通道的结果。因为我们将CH1接到3.3V,CH2接到0V,所以转换后,输出通道1的值是1024,输出通道2是0

对于Python用户

1) 在/home/pi下新建一个.py脚本文件,文件名随意(你爱咋咋地)

cd  ~

sudo  nano  readmcp3008.py

2) 编码

import time
import os
import RPi.GPIO as GPIO

# change these as desired - they're the pins connected from the
# SPI port on the ADC to the Cobbler
SPICLK = 11
SPIMISO = 9
SPIMOSI = 10
SPICS = 8
#DEBUG = 1

#setup function for some setup---custom function
def setup():
    GPIO.setwarnings(False)
    #set the gpio modes to BCM numbering
    GPIO.setmode(GPIO.BCM)
    # set up the SPI interface pins
    GPIO.setup(SPIMOSI, GPIO.OUT)
    GPIO.setup(SPIMISO, GPIO.IN)
    GPIO.setup(SPICLK, GPIO.OUT)
    GPIO.setup(SPICS, GPIO.OUT)
    
#print message at the begining ---custom function
def print_message():
    print ('|**********************************|')
    print ('|   Read MCP3008(3004) ADC value   |')
    print ('|   -----------------------------  |')
    print ('|    | ADC |           | Pi  |     |')
    print ('|    |-----|-----------|-----|     |')
    print ('|    | CS  | connect to| CE0 |     |')
    print ('|    | Din | connect to| MOSI|     |')
    print ('|    | Dout| connect to| MISO|     |')
    print ('|    | CLK | connect to| SCLK|     |')
    print ('|    | CH0 | connect to| 3.3V|     |')
    print ('|    | CH1 | connect to| GND |     |')
    print ('|   -----------------------------  |')
    print ('|                            OSOYOO|')
    print ('|**********************************|\n')
    print ('Program is running...')
    print ('Please press Ctrl+C to end the program...')
    print ('please input 0 to 7...')

# read SPI data from MCP3008 chip, 8 possible adc's (0 thru 7)
def readadc(adcnum, clockpin, mosipin, misopin, cspin):
        if ((adcnum > 7) or (adcnum < 0)):
                return -1
        GPIO.output(cspin, True)

        GPIO.output(clockpin, False)  # start clock low
        GPIO.output(cspin, False)     # bring CS low

        commandout = adcnum
        commandout |= 0x18  # start bit + single-ended bit
        commandout <<= 3    # we only need to send 5 bits here
        for i in range(5):
                if (commandout & 0x80):
                        GPIO.output(mosipin, True)
                else:
                        GPIO.output(mosipin, False)
                commandout <<= 1
                GPIO.output(clockpin, True)
                GPIO.output(clockpin, False)

        adcout = 0
        # read in one empty bit, one null bit and 10 ADC bits
        for i in range(12):
                GPIO.output(clockpin, True)
                GPIO.output(clockpin, False)
                adcout <<= 1
                if (GPIO.input(misopin)):
                        adcout |= 0x1

        GPIO.output(cspin, True)
        
        adcout >>= 1       # first bit is 'null' so drop it
        return adcout


#main function
def main():
    #print info
    print_message()
    analogChannel = int(input())
    if (analogChannel < 0) or (analogChannel > 7):
        print ('input error analogChannel number!')
        print ('please input 0 to 7...')
    else:
        adc = readadc(analogChannel, SPICLK, SPIMOSI, SPIMISO, SPICS)
        print ('analogChannel %d = %d'%(analogChannel,adc))

#define a destroy function for clean up everything after the script finished
def destroy():
    #release resource
    GPIO.cleanup()
#
# if run this script directly ,do:
if __name__ == '__main__':
    setup()
    try:
            main()
    #when 'Ctrl+C' is pressed,child program destroy() will be executed.
    except KeyboardInterrupt:
        destroy()

按键盘上的Ctrl +X根据提示输入Y保存退出。

完整的程序源码,可以在命令行运行一下命令获取

sudo  wget  https://osoyoo.com/driver/pi3_start_learning_kit_lesson_10/readmcp3008.py

3) 运行程序

sudo  python  ./readmcp3008.py

然后输入你想读取的输出通道,例如0,就在SHH终端输入0再回车,就能打印出输出通道0的数据。

4) 最终结果

在SSH终端输入sudo  python  ./readmcp3008.py后屏幕会打印出MCP3008与Pi的连接信息以及输入参数范围(0-7),如果输入的参数不在正确范围,程序会输入错误提示;如果输入的参数在0-7范围内,那就打印出对应端口的数据。