diff --git a/docs/examples/Pico/Pico_PST.ipynb b/docs/examples/Pico/Pico_PST.ipynb new file mode 100644 index 0000000000..4940f3442c --- /dev/null +++ b/docs/examples/Pico/Pico_PST.ipynb @@ -0,0 +1,499 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "# Pico SCPI labTool (PST)\n", + "\n", + "Want to work with a real, non-simulated instrument, but don't have high-end lab equipment on hand? Try a *****PST*****.\n", + "\n", + "Pico SCPI labTool (PST) is an ordinary,\n", + "$4 Raspberry Pi Pico\n", + "with specialized firmware from https://github.com/michaelstoops/pico_scpi_usbtmc_labtool.\n", + "It works on Pico 1 and 1W,\n", + "and may work on other RP2040 boards.\n", + "\n", + "It does not currently work on the Pico 2 series based on the RP2350 chip.\n" + ], + "id": "930a67b9b8758c72" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Setup\n", + "To get a PST:\n", + "1. Acquire a [Raspberry Pi Pico](https://www.raspberrypi.com/products/raspberry-pi-pico/) or compatible device.\n", + "2. Acquire [the firmware (~80 KB on GitHub)](https://github.com/michaelstoops/pico_scpi_usbtmc_labtool/releases/download/v0.01/pico_scpi_usbtmc_labtool.uf2)\n", + "This code was tested against v0.01.\n", + "3. Hold down the device's `BOOTSEL` button while plugging the Pico into USB.\n", + "It will appear in your system as a USB mass storage device called RPI-RP2.\n", + "4. Copy the firmware image to RPI-RP2.\n", + "The device will reboot.\n", + "Ignore your system's complaint about unsafely removing the device.\n", + "5. Your device is now a PST instrument.\n", + "You can power it up and down whenever you want.\n", + "The firmware is stored in FLASH ROM and will remain until you change it with the BOOTSEL procedure.\n", + "\n", + "You should be able to find the PST on your system under the name \"Pico SCPI labTool\" (product ID 0x41C0),\n", + "manufacturer \"Pico\" (vendor ID 0x1209).\n", + "It implements the USB Test and Measurement Class,\n", + "so it should work with any VISA library.\n", + "\n", + "The testing device appeared in NI VISA Interactive Control on MacOS as `USB0::0x1209::0x41C0::E660583883555B31::INSTR`.\n", + "Your device should have a similar ID,\n", + "but the fourth field is based on an internal hardware ID and will vary.\n" + ], + "id": "c6c2e29a0d88a864" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Breadboard (Optional)\n", + "\n", + "PST only requires the Pico board for basic functionality.\n", + "However, a breadboard is recommended for setting inputs and visualizing outputs.\n", + "Here's a [materials list](fritzing/Pico_PST.fz_bom.html).\n", + "\n", + "Here is a breadboard configuration that connects all four digital inputs to pushbutton switches.\n", + "When a button is pressed, it raises the respective input to high.\n", + "The four digital outputs are connected to current-limiting resistors and LEDs.\n", + "\n", + "The arrangement of the LEDs is harder to see in the breadboard rendering,\n", + "but they are meant to be arranged in the shape of a Microsoft logo.\n", + "Note that the long lead of an LED is the cathode and connected to the resistor.\n", + "The short leads of the LEDs overlap and are grounded.\n", + "\n", + "![image of a breadboard with Raspberry Pi Pico, four button switches, and four LEDs of red, green, blue, and yellow arranged like a Microsoft logo](fritzing/Pico_PST.fz_bb.svg)\n", + "\n", + "Here is the same circuit in schematic form, which is clearer about the LEDs connections:\n", + "\n", + "![schematic of a breadboard with Raspberry Pi Pico, four button switches, and four LEDs](fritzing/Pico_PST.fz_schem.svg)\n" + ], + "id": "502f6e2aa3cb851d" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## No Breadboard?\n", + "\n", + "If you don't have a breadboard available,\n", + "you can probe an output using a voltmeter with COM (ground probe) on the shell of the USB connector.\n", + "\n", + "There is a very helpful pinout diagram at https://pico.pinout.xyz/.\n", + "\n", + "You can also raise an input by touching either lead of a 3k ohm (or higher) resistor to the input pin and the 3.3V rail on pin 36.\n", + "It's possible to use a plain wire if you don't have a resistor,\n", + "but a resistor is safer.\n", + "In the case that the lead touches a low output or supply pin,\n", + "the resistor will limit current,\n", + "where a wire would burn out the GPIO pin or short the power supply.\n" + ], + "id": "9d9e49154c30a57" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Create the Instrument\n", + "\n", + "With your PST plugged in and SCPI ID in hand, let's try it out!" + ], + "id": "f60ccbf991b35f5d" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-02-13T10:09:13.243790Z", + "start_time": "2026-02-13T10:09:11.878874Z" + } + }, + "cell_type": "code", + "source": [ + "from time import sleep\n", + "\n", + "from qcodes_contrib_drivers.drivers.Pico.Pico_PST import PicoPST\n", + "\n", + "# Your device serial number will be different v--------------v\n", + "pico = PicoPST('pico', 'USB0::0x1209::0x41C0::E660583883555B31::INSTR')" + ], + "id": "b6c52a8722a43d6c", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Connected to: PICO PST (serial:E660583883555B31, firmware:01.00) in 0.04s\n" + ] + } + ], + "execution_count": 1 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Parameters\n", + "\n", + "Let's see what the parameters say they can do." + ], + "id": "e5d93fb7ea4c5c2e" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-02-13T08:33:28.596039Z", + "start_time": "2026-02-13T08:33:28.578090Z" + } + }, + "cell_type": "code", + "source": [ + "for io in [\n", + " pico.output_gp19,\n", + " pico.output_gp20,\n", + " pico.output_gp21,\n", + " pico.output_gp22,\n", + " pico.input_gp10,\n", + " pico.input_gp11,\n", + " pico.input_gp12,\n", + " pico.input_gp13,\n", + "]:\n", + " print(f\"{io}: {io.__doc__}\")" + ], + "id": "c2d0b8aa5b34aab4", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "pico_output_gp19: Digital OUTput GP19 (Pico pin 25)\n", + "pico_output_gp20: Digital OUTput GP20 (Pico pin 26)\n", + "pico_output_gp21: Digital OUTput GP21 (Pico pin 27)\n", + "pico_output_gp22: Digital OUTput GP22 (Pico pin 29)\n", + "pico_input_gp10: Digital INput GP10 (Pico pin 14)\n", + "pico_input_gp11: Digital INput GP11 (Pico pin 15)\n", + "pico_input_gp12: Digital INput GP12 (Pico pin 16)\n", + "pico_input_gp13: Digital INput GP13 (Pico pin 17)\n" + ] + } + ], + "execution_count": 5 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Outputs\n", + "\n", + "Let's try some outputs. This code should set all four outputs high (3.3V)." + ], + "id": "fa390d81280e17a8" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-02-13T08:34:08.798772Z", + "start_time": "2026-02-13T08:34:08.791090Z" + } + }, + "cell_type": "code", + "source": [ + "pico.output_gp19(True)\n", + "pico.output_gp20(True)\n", + "pico.output_gp21(True)\n", + "pico.output_gp22(True)" + ], + "id": "6706b3f2b1abcbeb", + "outputs": [], + "execution_count": 6 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "This code resets the outputs to low (0V).", + "id": "a354758f497f0693" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-02-13T08:35:44.883182Z", + "start_time": "2026-02-13T08:35:44.868137Z" + } + }, + "cell_type": "code", + "source": [ + "pico.output_gp19(False)\n", + "pico.output_gp20(False)\n", + "pico.output_gp21(False)\n", + "pico.output_gp22(False)" + ], + "id": "1db9c3e2fc0bb86f", + "outputs": [], + "execution_count": 7 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Inputs\n", + "\n", + "Inputs start in the disabled state.\n", + "That's not to say they don't work,\n", + "but depending on electrical conditions,\n", + "you may not get what you expect." + ], + "id": "11c64716fdd07826" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-02-13T09:22:50.705138Z", + "start_time": "2026-02-13T09:22:50.693916Z" + } + }, + "cell_type": "code", + "source": [ + "print(pico.inputs_enabled.__doc__+\"\\n\")\n", + "print(f\"Enabled: {pico.inputs_enabled.get()}\")" + ], + "id": "81dbf920f10244f1", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Enables or disables the use of inputs\n", + "\n", + "Parameter class:\n", + "\n", + "* `name` inputs_enabled\n", + "* `label` inputs_enabled\n", + "* `unit` \n", + "* `vals` \n", + "\n", + "Enabled: False\n" + ] + } + ], + "execution_count": 5 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "If you're going to use the inputs,\n", + "its best to explicitly enable them." + ], + "id": "65a8299bd7bcbfaa" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-02-13T09:23:15.793158Z", + "start_time": "2026-02-13T09:23:15.786206Z" + } + }, + "cell_type": "code", + "source": [ + "pico.inputs_enabled(True)\n", + "print(pico.inputs_enabled.get())" + ], + "id": "da2e89ee741545f", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "True\n" + ] + } + ], + "execution_count": 6 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Try pressing setting some combination of inputs and reading their state.", + "id": "fc2f99a9f12bff9c" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-02-13T10:09:37.073868Z", + "start_time": "2026-02-13T10:09:37.065018Z" + } + }, + "cell_type": "code", + "source": [ + "print(pico.input_gp10.get())\n", + "print(pico.input_gp11.get())\n", + "print(pico.input_gp12.get())\n", + "print(pico.input_gp13.get())" + ], + "id": "b6bb2dc09d88b4ed", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "True\n", + "True\n", + "False\n", + "True\n" + ] + } + ], + "execution_count": 4 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "If you're using the breadboard design, this code should illustrate the switches' state.", + "id": "e2308c0e5cf6611c" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-02-13T10:09:42.984126Z", + "start_time": "2026-02-13T10:09:42.977040Z" + } + }, + "cell_type": "code", + "source": [ + "def render(parameter):\n", + " return \"CLOSED\" if parameter.get() else \"OPEN \"\n", + "\n", + "print(f\"\"\"{render(pico.input_gp10)} {render(pico.input_gp11)}\n", + "\n", + "{render(pico.input_gp12)} {render(pico.input_gp13)}\"\"\")" + ], + "id": "b63a4cd0eb9fe4d6", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "OPEN CLOSED\n", + "\n", + "OPEN OPEN \n" + ] + } + ], + "execution_count": 6 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Automation\n", + "\n", + "The following code shows an example of setting and clearing the outputs in a timed sequence,\n", + "until the upper left button is held down.\n", + "If you don't have a breadboard,\n", + "you can raise GP10 (Pico pin 14).\n", + "\n", + "If you are using the breadboard design, watch the animation closely to reveal the hidden message!" + ], + "id": "3dc38952ab375f27" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-02-13T09:57:34.047765Z", + "start_time": "2026-02-13T09:57:33.945565Z" + } + }, + "cell_type": "code", + "source": [ + "pico.output_gp19(False)\n", + "pico.output_gp20(False)\n", + "pico.output_gp21(False)\n", + "pico.output_gp22(False)\n", + "\n", + "while not pico.input_gp10.get():\n", + " pico.output_gp19(False)\n", + " sleep(0.05)\n", + " pico.output_gp20(False)\n", + " pico.output_gp21(False)\n", + " sleep(0.05)\n", + " pico.output_gp22(False)\n", + "\n", + " sleep(0.05)\n", + "\n", + " pico.output_gp19(True)\n", + " sleep(0.05)\n", + " pico.output_gp20(True)\n", + " pico.output_gp21(True)\n", + " sleep(0.05)\n", + " pico.output_gp22(True)\n", + " sleep(0.05)\n", + "\n", + " sleep(0.5)\n", + "\n", + "pico.output_gp19(False)\n", + "pico.output_gp20(False)\n", + "pico.output_gp21(False)\n", + "pico.output_gp22(False)" + ], + "id": "31ef0fe6512cfcfc", + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'pico' is not defined", + "output_type": "error", + "traceback": [ + "\u001B[31m---------------------------------------------------------------------------\u001B[39m", + "\u001B[31mNameError\u001B[39m Traceback (most recent call last)", + "\u001B[36mCell\u001B[39m\u001B[36m \u001B[39m\u001B[32mIn[1]\u001B[39m\u001B[32m, line 1\u001B[39m\n\u001B[32m----> \u001B[39m\u001B[32m1\u001B[39m \u001B[43mpico\u001B[49m.output_gp19(\u001B[38;5;28;01mFalse\u001B[39;00m)\n\u001B[32m 2\u001B[39m pico.output_gp20(\u001B[38;5;28;01mFalse\u001B[39;00m)\n\u001B[32m 3\u001B[39m pico.output_gp21(\u001B[38;5;28;01mFalse\u001B[39;00m)\n", + "\u001B[31mNameError\u001B[39m: name 'pico' is not defined" + ] + } + ], + "execution_count": 1 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "If you don't have a breadboard, this might help:\n", + "\n", + "![Video of the Microsoft logo shimmering, and a finger pressing a button to stop it](shiny.gif)" + ], + "id": "16b941724c20e2e3" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "Did you see the message?\n", + "\n", + "That's right! It says \"hire me!\"" + ], + "id": "7f69d07c14bd1dbd" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/examples/index.rst b/docs/examples/index.rst index a7888dd118..0b5b24a390 100644 --- a/docs/examples/index.rst +++ b/docs/examples/index.rst @@ -13,3 +13,8 @@ To experiment with the examples you can download them directly from the git repo HP/* Cryomagnetics/* OxfordInstruments/* + Pico/* + +If you're looking for a place to start, consider Pico PST. +The driver is well-documented, +and you can get the minimum hardware for $4. diff --git a/src/qcodes_contrib_drivers/drivers/Pico/Pico_PST.py b/src/qcodes_contrib_drivers/drivers/Pico/Pico_PST.py new file mode 100644 index 0000000000..0f55fb9ebc --- /dev/null +++ b/src/qcodes_contrib_drivers/drivers/Pico/Pico_PST.py @@ -0,0 +1,82 @@ +from collections import namedtuple +import logging +from typing import TYPE_CHECKING + +from qcodes.validators import Bool + +if TYPE_CHECKING: + from typing_extensions import ( + Unpack, + ) + +from qcodes.instrument import ( + VisaInstrument, + VisaInstrumentKWArgs, +) + +OUT="out" +IN="in" + +log = logging.getLogger(__name__) + + +class PicoPST(VisaInstrument): + """ + QCoDeS instrument driver for the Pico SCPI USBTMC LabTool (PST) + =============================================================== + + Documentation pending. + """ + + default_terminator = "\n" + + # Table of digital inputs/outputs parameters + DigitalIO = namedtuple("DigitalIO","dir, scpi_num, gp, pico_pin") + _digital_ios = [ + DigitalIO(OUT, 0, 22, 29), + DigitalIO(OUT, 1, 14, 19), + DigitalIO(OUT, 2, 15, 20), + DigitalIO(IN, 0, 20, 29), + DigitalIO(IN, 1, 21, 27), + DigitalIO(IN, 2, 27, 32), + ] + + @staticmethod + def _int_as_bool(*args, **kwargs) -> bool: + return bool(int(*args, **kwargs)) + + def __init__( + self, name: str, address: str, **kwargs: "Unpack[VisaInstrumentKWArgs]" + ): + super().__init__(name, address, **kwargs) + + # Add digital IOs as parameters + for io in self._digital_ios: + parameter = self.add_parameter( + f"{io.dir}put_gp{io.gp}", + # Digital states are unitless + unit="", + set_cmd=f"DIGI:{io.dir.upper()}P{io.scpi_num} {{:d}}", + get_cmd=f"DIGI:{io.dir.upper()}P{io.scpi_num}?", + vals=Bool(), + get_parser=self._int_as_bool, + docstring=f"Turns GP{io.gp} (Pico pin {io.pico_pin}) on or off" if io.dir == IN else "Indicates whether GP20 (Pico pin 26) is on or off" + ) + parameter.__doc__= f"Digital {io.dir.upper()}put GP{io.gp} (Pico pin {io.pico_pin})" + setattr(self, f"output_{io.gp}", parameter) + + # Inputs must be enabled before they are used + self.inputs_enabled = self.add_parameter( + "inputs_enabled", + unit="", + set_cmd="STAT:OPER:DIGI:INP:ENAB {:d}", + get_cmd="STAT:OPER:DIGI:INP:ENAB?", + vals=Bool(), + get_parser=self._int_as_bool, + docstring="Enables or disables the use of inputs" + ) + """Inputs Enabled""" + + # Other functions exist but are not implemented here. + + self.connect_message() diff --git a/src/qcodes_contrib_drivers/drivers/Pico/__init__.py b/src/qcodes_contrib_drivers/drivers/Pico/__init__.py new file mode 100644 index 0000000000..e69de29bb2