Raspberry Pi Pico project 2 - MCP3008

This post show you how to drive the MCP3008 8-channel ADC using a Raspberry Pi Pico. I'll start with a personally embarrassing and annoyingly relevant story. I'll also tell you about a minor 'gotcha' when using SPI on the Pico, and how you can avoid it. Finally, I've include the code and fritzing diagram I used to test the interface.

First, the story.

Back in 2012, when the Raspberry Pi was fresh and new, I was running a startup called Quick2Wire.  We made add-on boards for the Pi. Just before Christmas we sent out our first batch of boards to a small army of beta-testers.

The beta boards didn't work.


Somehow a single trace on the pcb had got deleted and one of the GPIO pins was isolated. The boards were still usable and it wasn't too hard to solder in a jumper wire to replace the missing trace, but it was very embarrassing.

It wasn't just that we'd shipped boards with a defect. The really annoying thing was that I'd set up an  automated loop-back test harness, but in a fit of over-confidence I just did a quick manual test on the board before we sent them out.

The automated test would have caught the problem which my quick manual test failed to spot.

I'll come back to loop-back testing below.

Back to the Pico

One of my goals for my Pico mini-projects was to verify that I could drive I2C and SPI chips using the Pico's MicroPython. I2C and SPI protocols are really useful.They let you drive all kinds of interesting sensors and actuators, and that means that they enable lots and lots of interesting applications.

There are an SPI example in the excellent Getting Started guide, and it gave me some useful ideas. There's a little more information in the Pico Python SDK documentation, and yet more clues in the Pico pinout diagram. I also found a CircuitPython driver for the MCP3008 on Adafruit's GitHub repository.

I reckoned that I now had enough information to wire up and drive an MCP3008 chip. The MCP3008 is an 8-channel  ADC (analogue-to-digital) converter. The Pico has 3 ADC pins, but sometimes you need more. The 3008 has a simple, well documented SPI interface, and I had a few in my parts bin, so it was an obvious choice for the test.

I quickly wrote some code, and sure enough it worked. I posted it as a gist on GitHub and tweeted about it.

Some time later that day I got a comment from Matt Trentini, one of the members of  a Facebook group that covers the Pico and other MicroPython controllers. He suggested a small refactoring to the code. It clearly made the code better, and I decided to implement it.

Loopback tests

Some people are smart enough to be able to refactor code without tests but I am not one of them.

I decided to create a loop-back test. In a loop-back test the UUT (unit under test) provides an output which is connected to one of its inputs. In other words, the output is looped back  to the input. When you change the output, the input should change. If everything works as expected, the loop-back test passes.

In this case, I just connect a GPIO pin to one of the MCP3008 inputs. When the GPIO output is high the MCP3008 should read 3.3 volts; when it's low, the MCP3008 should read 0 volts.
I set up a loop-back test and ran it. It worked.

I did the refactoring and the test stopped working.

What had I done wrong?

SPI defaults on the PICO


The Pico has two hardware SPI busses, and you can access each buss from several different pins. An SPI chip needs three data inputs and one chip selection input. The data inputs are traditionally called SCK, MISO and MOSI. SCK carries a clock signal. MISO carries data from the chip to the Pico, and MSOI carries data from the Pico to the chip. Because the Pico can be a controller or be controlled by SPI,  the Pico's pinout diagram calls MISO  SPI RX and calls MOSI  SPI TX.

The Pinout diagram covers both SPI busses, and the example in the Getting Started book shows an example that uses Pin 2 as SCK, Pin 3 as TX and Pin 4 as RX.

I wrongly assumed that those were the default pins. In the first bit of code I specified the pins explicitly. In the refactored code, I used the default SPI constructor.

After hours of puzzlement I discovered from the Pico Python SDK documentation that the default pins are 6, 7 and 4, not 2, 3 and 4 as used in the book example. It was easier to change my code than to change my wiring, so the code on GitHub now specifies the pins explicitly.

Here's the code for the MCP3008 class.
"""
MicroPython Library for MCP3008 8-channel ADC with SPI

Datasheet for the MCP3008: https://www.microchip.com/datasheet/MCP3008

This code makes much use of Adafruit's CircuitPython code at
https://github.com/adafruit/Adafruit_CircuitPython_MCP3xxx
adapted for MicroPython.

Tested on the Raspberry Pi Pico.

Thanks, @Raspberry_Pi and @Adafruit, for all you've given us!
"""

import machine


class MCP3008:

    def __init__(self, spi, cs, ref_voltage=3.3):
        """
        Create MCP3008 instance

        Args:
            spi: configured SPI bus
            cs:  pin to use for chip select
            ref_voltage: r
        """
        self.cs = cs
        self.cs.value(1) # ncs on
        self._spi = spi
        self._out_buf = bytearray(3)
        self._out_buf[0] = 0x01
        self._in_buf = bytearray(3)
        self._ref_voltage = ref_voltage

    def reference_voltage(self) -> float:
        """Returns the MCP3xxx's reference voltage as a float."""
        return self._ref_voltage

    def read(self, pin, is_differential=False):
        """
        read a voltage or voltage difference using the MCP3008.

        Args:
            pin: the pin to use
            is_differential: if true, return the potential difference between two pins,


        Returns:
            voltage in range [0, 1023] where 1023 = VREF (3V3)

        """

        self.cs.value(0) # select
        self._out_buf[1] = ((not is_differential) << 7) | (pin << 4)
        self._spi.write_readinto(self._out_buf, self._in_buf)
        self.cs.value(1) # turn off
        return ((self._in_buf[1] & 0x03) << 8) | self._in_buf[2]

And here's the code for the loop-back test.
from machine import Pin, SPI
from time import sleep, sleep_ms
from mcp3008 import MCP3008

spi = SPI(0, sck=Pin(2),mosi=Pin(3),miso=Pin(4), baudrate=100000)
cs = Pin(22, Pin.OUT)
cs.value(1) # disable chip at start

square = Pin(21, Pin.OUT)

chip = MCP3008(spi, cs)

while True:
    square.value(1)
    actual = chip.read(0)
    print(actual)
    sleep(0.5)
    square.value(0)
    actual = chip.read(0)
    print(actual)
    sleep(0.5)

Finally, here's the fritzing diagram for the loopback wiring.




I've several more mini-projects to cover in the next day or so. If you want to keep track of what I'm up to, follow me (@rareblog) on twitter.


Comments

  1. Great to hear from you again. I'm still a Quick2Wire user and I'll give this a try.
    Tony Goodhew

    ReplyDelete
    Replies
    1. I'm glad we're back in touch - and thanks again for the great instructable!

      Delete

Post a Comment

Popular posts from this blog

Controlling a Raspberry Pi Pico remotely using PySerial

Five steps to connect Jetson Nano and Arduino