RP2040 WiFi using Microchip ATWINC1500 module: part 2

Server sockets

In part 1, we got as far as connecting an ATWINC1500 or 1510 module to a WiFi network; now it is time to do something vaguely useful with it.


A network interface is frequently referred to as a ‘socket’ interface, so first I’d better define what that is.

A socket is a logical endpoint for network communication, and consists of an IP address, and a port number. The IP address is often assigned automatically at boot-time, using Dynamic Host Configuration Protocol (DHCP) as in part 1, but can also be pre-programmed into the unit (a ‘static’ address).

The 16-bit port number further subdivides the functionality within that IP address, so one address can support multiple simultaneous conversations (‘connections’). Furthermore, specific port numbers below 1024 are generally associated with specific functions; for example, an un-encrypted Web server normally uses port 80, whilst an encrypted server is on port 443. Port numbers 1024 and above are generally for user programs.

Clients and servers, UDP and TCP

A sever runs all the time, waiting for a client to contact it; the client is responsible for initiating the contact and providing some data, probably in the form of a request. The server returns an appropriate response, then either side may terminate the connection; the client may end it because it has received enough data, or the server because there are limits on the maximum number of simultaneous clients it can service.

There are 2 fundamental communication methods in TCP/IP: User Datagram Protocol (UDP) and Transmission Control Protocol (TCP).

UDP is the simpler of the two, and involves sending a block of data to a given socket (port and IP address) with no guarantee that it will arrive. TCP involves sending a stream of data to a socket; it includes sophisticated retry mechanisms to ensure that the data arrives.

There are those in the networking community who shun UDP, because they think the unreliability makes is useless; I disagree, and think there are various use-cases where the simple block-based transfer is perfectly adequate, possibly overlaid with a simple retry mechanism, so we’ll start with a simple UDP server.

UDP server

The simplest UDP server is stateless, i.e. it doesn’t store any information about the client; it just responds to any request it receives. This means that a single socket can handle multiple clients, unlike TCP which requires a unique socket for each client it is communicating with.

For a classic C socket interface, the steps would be:

  1. Create a datagram socket using socket()
  2. Bind to the socket to a specific port using bind()
  3. When a message is received on that port, get the data, return address and port number using recvfrom()
  4. Send response data to the remote address and port number using sendto()
  5. Go to step 3

The code driving the ATWINC1500 module does the same job, but the function calls are a bit different, as they reflect the messages sent to & received from the WiFi module:

  1. Initialise a socket structure for UDP
  2. Send a BIND command to the module, with the port number
  3. Receive a BIND response
  4. Send a RECVFROM command to the module
  5. Wait until a RECVFROM response is received, get the data, return address & port number
  6. Send a SENDTO command to the module with the response data, return address & port number
  7. Go to step 4

Note that there may be a very long wait between steps 4 and 5, if there are no clients contacting the server. Fortunately the module will signal the arrival of a message by asserting the interrupt request (IRQ) line, so the RP2040 CPU can proceed with other tasks while waiting.

UDP Reception

There are 4 steps when the module receives a packet (‘datagram’) from a remote client:

  1. Get the group ID and operation. This identifies the message type; for UDP it will generally be a response to a RECVFROM request, but it could be something completely different. My software combines the group ID and operation into a single 16-bit number.
  2. Get the operation-specific header. This is generally 16 bytes or less, and in the case of RECVFROM, gives the IP address and port number of the sender, also a pointer & offset to the user data in the buffer.
  3. Get the user data. The application doesn’t need to fetch all the incoming data; for example, in the case of a Web server, it might just get the first line of the page request, and discard all the other information.
  4. Handle socket errors. If there is an error, the data length-value will be negative, and the code must take appropriate action, such as closing and re-opening the server socket. Since a UDP socket is connectionless, it generally won’t see many errors, but a TCP socket will flag an error every time a client closes an active connection.

For RECVFROM, the step 1 & 2 headers are:

// HIF message header
typedef struct {
    uint8_t gid, op;
    uint16_t len;

// Operation-specific header
typedef struct {
    SOCK_ADDR addr;
    int16_t dlen; // (status)
    uint16_t oset;
    uint8_t sock, x;
    uint16_t session;

Having fetched these two blocks, control is passed to a state machine that takes appropriate action. If we’ve just received an indication that DHCP has succeeded, then we bind the server sockets.

    if (gop==GOP_DHCP_CONF)
        for (sock=MIN_SOCKET; sock<MAX_SOCKETS; sock++)
            sp = &sockets[sock];
            if (sp->state==STATE_BINDING)
                put_sock_bind(fd, sock, sp->localport);

When we get a message indicating the binding has succeeded, then if it is a TCP socket, we need to send a LISTEN command. If UDP, we can just send a RECVFROM, and wait for data to arrive. We can tell whether the socket is TCP or UDP by looking at the socket number; the lower numbers are TCP, and higher are UDP.

    else if (gop==GOP_BIND && (sock=rmp->bind.sock)<MAX_SOCKETS &&
        sock_state(sock, STATE_BOUND);
        if (sock < MIN_UDP_SOCK)
            put_sock_listen(fd, sock);
            put_sock_recvfrom(fd, sock);

If a UDP server, we may now get a RECVFROM response, indicating that a packet (‘datagram’) has arrived. If so, we save the return socket address (IP and port number), call a handler function, then send another RECVFROM request.

    else if (gop==GOP_RECVFROM && (sock=rmp->recv.sock)<MAX_SOCKETS &&
        memcpy(&sp->addr, &rmp->recv.addr, sizeof(SOCK_ADDR));
        if (sp->handler)
            sp->handler(fd, sock, rmp->recv.dlen);
        put_sock_recvfrom(fd, sock);

A very simple handler just echoes back the incoming data:

uint8_t databuff[MAX_DATALEN];

// Handler for UDP echo
void udp_echo_handler(int fd, uint8_t sock, int rxlen)
    if (rxlen>0 && get_sock_data(fd, sock, databuff, rxlen))
        put_sock_sendto(fd, sock, databuff, rxlen);

UDP client for testing

For testing, I use a simple UDP client written in Python, that can run on a Raspberry Pi, or any PC running Linux or Windows. It sends a message every second, and checks for a response. You’ll need to change the IP address to match the DCHP value given by the module.

# Simple Python client for testing UDP server
import socket, time

ADDR = ""
PORT = 1025
MESSAGE = b"Test %u"

def hex_str(bytes):
    return " ".join([("%02x" % int(b)) for b in bytes])

print("Send to UDP %s:%s" % (ADDR, PORT))
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
count = 1
while True:
    msg = MESSAGE % count
    sock.sendto(msg, (ADDR, PORT))
    print("Tx %u: %s" % (len(msg), hex_str(msg)))
    count += 1
        data = sock.recvfrom(1000)
        data = None
    if data:
        bytes, addr = data
        s = hex_str(bytes)
        print("Rx %u: %s\n" % (len(bytes), s))

TCP server

A TCP connection is more complex than UDP, since the module firmware must keep track of the data that is sent & received, in order to correct any errors. The steps for a classic C socket interface would be:

  1. Create a ‘stream’ socket using socket()
  3. Set the socket to wait for incoming connections using listen()
  4. When a connection request is received on the main socket, open a new socket for the data using accept()
  5. When data arrives on the new socket, get it using recv()
  6. Send response data using send()
  7. If socket error or transfer complete, close socket. Otherwise go to step 5

The corresponding operations for the WiFi module are:

  1. Initialise a socket structure for TCP
  3. Receive a BIND response
  4. Send a LISTEN command
  5. Receive a LISTEN response
  6. Receive an ACCEPT notification when a connection request arrives on the main socket. Save the new socket number.
  7. Send a RECV command on the new socket.
  8. Receive a RECV response when data arrives on the new socket
  9. Send response data using SEND
  10. Go to step 7, or close the new socket

TCP reception

The first step (binding a socket to a port number) is the same as for UDP, but then we send a LISTEN command, which activates the socket to receive incoming connections. When a client connects, we get an ACCEPT response containing 2 socket numbers; the first is the one that we used for the original BIND command, and the second is a new socket that will be used for the data transfer; we need to issue a RECV on this socket to get the user data.

    else if (gop==GOP_ACCEPT &&
             (sock=rmp->accept.listen_sock)<MAX_SOCKETS &&
             (sock2=rmp->accept.conn_sock)<MAX_SOCKETS &&
        memcpy(&sockets[sock2].addr, &rmp->recv.addr, sizeof(SOCK_ADDR));
        sockets[sock2].handler = sockets[sock].handler;
        sock_state(sock2, STATE_CONNECTED);
        put_sock_recv(fd, sock2);

When data is available, the RECV command will return, and we can call a data handler function, then send another RECV for more data. Alternatively, if the data length is negative, then there is an error, and the socket needs to be closed. This isn’t necessarily as bad as it sounds; the most common reason is that the client has closed the connection, and we just need to erase the 2nd socket for future use.

    else if (gop==GOP_RECV && (sock=rmp->recv.sock)<MAX_SOCKETS &&
        if (sp->handler)
        if (rmp->recv.dlen > 0)
            put_sock_recv(fd, sock);

The TCP data handler is again a simple echo-back of the incoming data, but with an added complication: if the data length is negative, there has been an error. This isn’t as bad as it sounds; the most common error is that the client has closed the TCP connection, so the server must also close its data socket, to allow it to be re-used for a new connection.

// Handler for TCP echo
void tcp_echo_handler(int fd, uint8_t sock, int rxlen)
    if (rxlen < 0)
        put_sock_close(fd, sock);
        put_sock_send(fd, sock, databuff, rxlen);

TCP client for testing

This is similar to the UDP client; it can run on a Raspberry Pi or PC, running Linux or Windows:

# Simple Python client for testing TCP server
import socket, time

ADDR = ""
PORT = 1025
MESSAGE = b"Test %u"

def hex_str(bytes):

print("Send to TCP %s:%s" % (ADDR, PORT))
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((ADDR, PORT))
count = 1
while True:
    msg = MESSAGE % count
    count += 1
        data = sock.recv(1000)
        data = None
    if data:
        s = hex_str(data)
        print("Rx %u: %s\n" % (len(data), s))

Source files

The C source files are in the ‘part2’ directory on  Github here

The default network name and passphrase are “testnet” and “testpass”; these must be changed to match your network, then the code will need to be rebuilt & run using the standard Pico devlopment environment.

The default TCP & UDP port numbers are 1025, and the Python programs I’ve provided can be used to perform simple simple echo tests, providing the IP address is modified to match that given when the Pico joins the network.

python tcp_tx.py
Send to TCP
Tx 6: 54 65 73 74 20 31
Rx 6: 54 65 73 74 20 31

Tx 6: 54 65 73 74 20 32
Rx 6: 54 65 73 74 20 32
..and so on..

python udp_tx.py
Send to UDP
Tx 6: 54 65 73 74 20 31
Rx 6: 54 65 73 74 20 31

Tx 6: 54 65 73 74 20 32
Rx 6: 54 65 73 74 20 32
..and so on..


RP2040 WiFi using Microchip ATWINC1500 module

Part 1: joining a network

WINC1500 modules

The Raspberry Pi Pico is an incredibly useful low-cost micro-controller module based on the RP2040 CPU, but at the time of writing, there is a major omission: there is no networking capability.

This project adds low-cost wireless networking to the Pi Pico, and any other RP2040 boards. The There are various modules on the market that could be used for this purpose; I have chosen the Microchip ATWINC1500 or 1510 modules as they low-cost, have an easy hardware interface (4-wire SPI), and feature a built-in TCP/IP software stack, which significantly reduces the amount of software needed on the RP2040.

The photo above shows the module mounted on an Adafruit breakout board, and the module itself; this is the variant with a built-in antenna, but there is also a version with an antenna connector, that allows an external antenna to be used.

The only difference between the ATWINC1500 and 1510 modules is that the latter have larger flash memory size (1 MB, as opposed to 0.5 MB). There is also an earlier series of low-level interface modules named ATWILC; I’m not using them, as the built-in TCP/IP software of the ATWINC saves a lot of code complication on the RP2040.

Hardware connections

Pi Pico and WiFi module

For simplicity, I have used the Adafruit breakout board, but it is possible to directly connect the module to the Pico, powered from its 3.3V supply.

Wiring Pico to Adafruit WINC1500 breakout
Pi Pico pins
SCK     18     SPI clock
MOSI    19     SPI data out
MISO    16     SPI data in
CS      17     SPI chip select
WAKE    20     Module wake
EN      20     Module enable
RESET   21     Module reset
IRQ     22     Module interrupt request

No extra components are needed, if the wiring to the module is kept short, i.e. 3 inches (76 mm).

SPI on the RP2040

Initialising the SPI interface on the RP2040 just involves a list of API function calls:

#define SCK_PIN     18
#define MOSI_PIN    19
#define MISO_PIN    16
#define CS_PIN      17
#define WAKE_PIN    20
#define RESET_PIN   21
#define IRQ_PIN     22

// Initialise SPI interface
void spi_setup(int fd)
    spi_init(SPI_PORT, SPI_SPEED);
    spi_set_format(SPI_PORT, 8, SPI_CPOL_0, SPI_CPHA_0, SPI_MSB_FIRST);
    gpio_set_function(MISO_PIN, GPIO_FUNC_SPI);
    gpio_set_function(CS_PIN,   GPIO_FUNC_SIO);
    gpio_set_function(SCK_PIN,  GPIO_FUNC_SPI);
    gpio_set_function(MOSI_PIN, GPIO_FUNC_SPI);
    gpio_set_dir(CS_PIN, GPIO_OUT);
    gpio_put(CS_PIN, 1);
    gpio_set_dir(WAKE_PIN, GPIO_OUT);
    gpio_put(WAKE_PIN, 1);
    gpio_set_dir(IRQ_PIN, GPIO_IN);
    gpio_set_dir(RESET_PIN, GPIO_OUT);
    gpio_put(RESET_PIN, 0);
    gpio_put(RESET_PIN, 1);

When using the standard SPI transfer API function, I found that occasionally the last data bit wasn’t being received correctly. The reason was that the API function returns before the transfer is complete; the clock signal is still high, and needs to go low to finish the transaction. To fix this, I inserted a loop that waits for the clock to go low, before negating the chip-select line.

// Do SPI transfer
int spi_xfer(int fd, uint8_t *txd, uint8_t *rxd, int len)
    gpio_put(CS_PIN, 0);
    spi_write_read_blocking(SPI_PORT, txd, rxd, len);
    while (gpio_get(SCK_PIN)) ;
    gpio_put(CS_PIN, 1);

Interface method

The WiFi module has its own processor, running proprietary code; it is supplied with a suitable binary image already installed, so will start running as soon as the module is enabled.

Pico WINC1500 block diagram

The module has a Host Interface (HIF) that the Pico uses for all communications; it is a Serial Peripheral Interface (SPI) that consists of a clock signal, incoming & outgoing data lines (MOSI and MISO), and a Chip Select, also known as a Chip Enable. The Pico initiates and controls all the HIF transfers, but the module can request a transfer by asserting an Interrupt Request (IRQ) line.

The module is powered up by asserting the ‘enable’ line, then briefly pulsing the reset line. This ensures that there is a clean startup, without any complications caused by previous settings.

There are 2 basic methods to transfer data between the PICO and the module; simple 32-bit configuration values can be transferred as register read/write cycles; there is a specific format for these, which includes an acknowledgement that a write cycle has succeeded. The following logic analyser trace shows a 32-bit value of 0x51 being read from register 0x1070; the output from the CPU is MOSI, and the input from the module is MISO.

ATWINC1500 register read cycle

Now the corresponding write cycle, where the CPU is writing back a value of 0x51 to the same 32-bit register.

ATWINC1500 register write cycle

There are a few unusual features about these transfers.

  • The chip-select (CS) line doesn’t have to be continuously asserted during the transfer, it need only be asserted whilst a byte is actually being read or written.
  • The command value is CA hex for a read cycle, and C9 for a write.
  • The module echoes back the command value plus 2 bytes for a read (CA 00 F3), or plus 1 byte for a write (C9 00), to indicate it has been accepted.
  • The register address is 24-bit, big-endian (most significant byte first)
  • The data value is 32-bit, little-endian in the read cycle (51 00 00 00), and big-endian in the write cycle (00 00 00 50).

The last point is quite remarkable, and when starting on the code development, I had great difficulty believing it could be true. The likely reason is that the SPI transfer is is big-endian as defined in the Secure Digital (SD) card specification, but the CPU in the module is little-endian. So the firmware has to either do a byte-swap on every response message, or return everything using the native byte-order, with this result.

In addition to reading & writing single-word registers, the software must read & write blocks of data. This involves some negotiation with the module firmware, since that manages the allocation & freeing of the necessary storage space in the module. For example, the procedure for a block write is:

  1. Request a buffer of the required size
  2. Receive the address of the buffer from the module
  3. Write one or more data blocks to the buffer
  4. Signal that the transfer is complete

Reading is similar, except that the first step isn’t needed, as the buffer is already available with the required data.


The above transfer mechanism is used to send commands to the module, and receive responses back from it; there is generally a one-to-one correspondence between the command and response, but there may be a significant delay between the two. For example, the ‘receive’ command requests a data block that has been received over the network, but if there is none, there will be no response, and the command will remain active until something does arrive.

The commands are generally referred to as ‘operations’, and they are split into groups:

  1. Main
  2. Wireless (WiFi)
  3. Internet Protocol (IP)
  4. Host Interface (HIF)
  5. Over The Air update (OTA)
  6. Secure Socket Layer (SSL)
  7. Cryptography (Crypto)

Each operation is assigned a number, and there is some re-use of numbers within different groups, for example a value of 70 in the WiFi group is used to enable Acess Point (AP) mode, but the same value in the IP group is a socket receive command. To avoid this possible source of confusion, my code combines the group and operation into a single 16-bit value, e.g.

// Host Interface (HIF) Group IDs
#define GID_MAIN        0
#define GID_WIFI        1
#define GID_IP          2
#define GID_HIF         3

// Host Interface operations with Group ID (GID)
#define GIDOP(gid, op) ((gid << 8) | op)
#define GOP_DHCP_CONF       GIDOP(GID_WIFI, 50)
#define GOP_BIND            GIDOP(GID_IP,   65)
..and so on..

To invoke an operation on the module, you must first send a 4-byte header that gives an 8-bit operation number, 8-bit group, and 16-bit message length.

typedef struct {
    uint8_t gid, op;
    uint16_t len;

The next 4 bytes of the message are unused, so can either be sent as zeros, or just skipped. Then there is the command header, which varies depending on the operation being performed, but are often 16 bytes or less, for example the IP ‘bind’ command:

// Address field for socket, network order (MSbyte first)
typedef struct {
    uint16_t family, port;
    uint32_t ip;

// Socket bind command, 12 bytes
typedef struct {
    SOCK_ADDR saddr;
    uint8_t sock, x;
    uint16_t session;

I’ll be discussing the IP operations in detail in the next part.

The interrupt request (IRQ) line is pulled low by the module to indicate that a response is available; for simplicity, my code polls this line, and calls an interrupt handler.

if (read_irq() == 0)

Joining a network

I’ll start with the most common use-case; joining a network that uses WiFi Protected Access (WPA or WPA2), and obtaining an IP address using Dynamic Host Configuration Protocol (DHCP). This is remarkably painless, since the module firmware does all of the hard work, but first we have to tackle the issue of firmware versions.

As previously explained, the module comes pre-loaded with firmware; at the time of writing, this is generally version 19.5.2 or 19.6.1. There is a provision for re-flashing the firmware to the latest version, but for the time being I’d like to avoid that complication, so the code I’ve written is compatible with both versions.

The reason that this matters is that 19.6.1 introduced a new method for joining a network, with a new operation number (59, as opposed to 40). Fortunately the newer software can still handle the older method, so that is what I’ll be using by default, though there is a compile-time option to use the new one, if you’re sure the module has the newer firmware.

The code to join the network is remarkably brief, just involving some data preparation, then calling a host interface transfer function to send the data. It searches across all channels to find a signal that matches the given Service Set Identifier (SSID, or network name). A password string (WPA passphrase) is also given; if this is a null value, the module will attempt to join an ‘open’ (insecure) network, but there are very obvious security risks with this, so it is not recommended.

// Join a WPA network, or open network if null password
bool join_net(int fd, char *ssid, char *pass)
    CONN_HDR ch = {pass?0x98:0x2c, CRED_STORE, ANY_CHAN, strlen(ssid), "",
                   pass?AUTH_PSK:AUTH_OPEN, {0,0,0}};
    PSK_DATA pd;

    strcpy(ch.ssid, ssid);
    if (pass)
        memset(&pd, 0, sizeof(PSK_DATA));
        strcpy(pd.phrase, pass);
        pd.len = strlen(pass);
        return(hif_put(fd, GOP_CONN_REQ_NEW|REQ_DATA, &ch, sizeof(CONN_HDR),
               &pd, sizeof(PSK_DATA), sizeof(CONN_HDR)));
    return(hif_put(fd, GOP_CONN_REQ_NEW, &ch, sizeof(CONN_HDR), 0, 0, 0));
    OLD_CONN_HDR och = {"", pass?AUTH_PSK:AUTH_OPEN, {0,0}, ANY_CHAN, "", 1, {0,0}};

    strcpy(och.ssid, ssid);
    strcpy(och.psk, pass ? pass : "");
    return(hif_put(fd, GOP_CONN_REQ_OLD, &och, sizeof(OLD_CONN_HDR), 0, 0, 0));

Running the code

There are 3 source files in the ‘part1’ directory on  Github here:

  • winc_pico_part1.c: main program, with RP2040-specific code
  • winc_wifi.c: module interface
  • winc_wifi.h: module interface definitions

The default network name and passphrase are “testnet” and “testpass”; these will have to be changed to match your network.

Normally I’d provide a simple Pi command-line to compile & run the files, but this is considerably more complex on the Pico; you’ll have to refer to the official documentation for setting up the development tools. I’ve provided a simple cmakelists file, that may need to be altered to suit your environment.

There is a compile-time ‘verbose’ setting, which regulates the amount of diagnostic information that is displayed on the console (serial link). Level 1 shows the following:

Firmware 19.5.2, OTP MAC address F8:F0:05:xx.xx.xx
Interrupt gid 1 op 44 len 12 State change connected
Interrupt gid 1 op 50 len 28 DHCP conf gate

[or if the network can't be found]
Interrupt gid 1 op 44 len 12 State change fail

Verbose level 2 lists all the register settings as well, e.g.

Rd reg 1000: 001003a0
Rd reg 13f4: 00000001
Rd reg 1014: 807c082d
Rd reg 207bc: 00003f00
Rd reg c000c: 00000000
Rd reg c000c: 10add09e
Wr reg 108c: 13521330
Wr reg 14a0: 00000102
..and so on..

Level 3 also includes hex dumps of the data transfers.

Socket interface

Part 2 describes the socket interface, with TCP and UDP servers here.


PicoReg: real-time diagnostics for the Pi Pico using SWD

PicoReg PyQt display

The Raspberry Pi Pico CPU (RP2040) has a remarkably complex set of peripherals, and this is reflected in the very large number of control registers (1,116).

To debug a C or Python application, it can be very helpful to know the values in these registers; instead of adding ‘print’ calls, or using a heavyweight debugger, you can use a 3-wire connection between the Pico and a Raspberry Pi, and view the state of any register without modifying or disrupting the software. This magic is achieved using the Single Wire Debug (SWD) interface; it is mainly used for reprogramming the Flash memory of the Pico, but can do a lot more – it acts as a transparent window into the I/O subsystems, that is completely independent of the CPU.

This capability can be accessed using software tools such as OpenOCD, GDB, and Eclipse, but I wanted to create something much simpler, and easier to use. The end-result is PicoReg, which is a pure-Python program that runs on any Raspberry Pi. In addition to the SWD interface, it can access the standard System View Description (SVD) file, to give a description of every register in considerable detail.

The end-result is a simple-to-use software tool that gives a valuable insight into the inner workings of the Pico.



Pi -to-Pico SWD connections

Only 3 wires are needed; a ground connection, SWCLK and SWDIO.

Any I/O pins on the Pi could be used; for ease of identification, I’ve chosen BCM pin numbers 20 and 21. These should be connected to the SWCLK and SWDIO pins on the edge of the Pico; keep the wires as short as possible, ideally a 150 mm (6 inches) or less.

The pins are defined at the top of picoreg_gpio.py, so can easily be changed to any others, but you need to be sure there is no conflict with other device drivers, such as serial, SPI etc.

# Clock and data BCM pin numbers
CLK_PIN         = 20
DAT_PIN         = 21

The Pico will need to be powered as normal, for example using a 5V supply into the USB socket, or if the Pico USB interface is not connected, linking the the 5V pin on the Pi to the VSYS pin on the Pico.


There are 2 Python files, and one database file:

  • picoreg_gpio.py. The low-level interface code, with a very simple command-line interface.
  • picoreg_qt.py. A PyQt application with full GUI, that uses picoreg_gpio as a low-level interface.
  • rp2040.svd. The System View Description file provided by the Raspberry Pi organisation, that describes all the peripheral registers in XML format

The GUI translates register names (such as TIMER.ALARM3) into physical addresses (such as 0x4005401c) using the SVD file; if it is missing, the GUI won’t work. The software performs best on a Pi 3 or 4; it will work on earlier devices, but is a bit slower.

The files can be found on github here; copy them to any convenient directory, then install PyQt5 on the Pi. The current version at the time of writing is 5.11.3:

sudo apt update
sudo apt install python3-pyqt5

That is all you need to do!

Running the GUI

Start the GUI from a console:

python3 picoreg_qt.py

The application will respond “Loading rp2040.vsd”, then after a short delay, will show the GUI. You may also see some warning messages from PyQt, but these are harmless.

Picoreg initial display

The controls are:

  • Core number. The Pico is a 2-core device, and this allows you to select core 0 or 1. There is no practical difference when using Picoreg to look at I/O, since the peripheral registers are common to both cores.
  • Verbose. This allows you to see the SWD messages that Picoreg is sending, and the responses obtained; useful when there are problems with the link.
  • Connect. This starts communication with the Pico, and verifies the identity of the RP2040 CPU. When the link is broken, it is necessary to re-connect before doing any register accesses.
  • Single. When connected, this button takes a single reading from the highlighted register.
  • Run. When connected, this button starts a continuous cycle of reading from the highlighted register, at 5 readings per second.

Note that this initial display is not Raspberry-Pi-specific; it will run on any PC, even under Windows. This allows you to browse the register database on any convenient machine, though the low-level I/O code is Pi-specific (using the RPI.GPIO module), so the SWD code only runs on a Pi.

The upper display is a tree structure, containing the peripherals, registers, and fields within the registers. By default it is alphabetically sorted, click on the ‘base’ header, to sort by address. The lower display shows general information, and a description of the selected register.

If the SVD database file is damaged or missing, Picoreg will report a syntax error; check that the file is in the same directory as picroreg_qy.py, and restart.

Click on ‘connect’ and if all is well, the following will be displayed.

SWD connection restart
DPIDR 0x0bc12477

The Debug Port Identification Register value shows that Picoreg connection has succeeded. The software makes 3 attempts to do this, and if unsuccessful, the most likely cause is incorrect wiring, or the Pico board not being powered up.

You can navigate around the register display using mouse (single or double-click) or keyboard, as is usual for such tree displays.

If you have a new un-programmed Pico, try navigating to the TIMER.TIMELR register, and hit ‘Run’.

Viewing timer value

You should see a rapidly-changing display of the current timer value; you can then move the cursor to other registers, whilst the data collection is still running; the register value under the cursor will be updated, while previously-accessed register values are static. There is a flashing indication in the bottom-left corner of the window to show that data collection is running.

Debugging an application

Load the following MicroPython application onto the Pico

# Simple test of O/P and ADC
from machine import Pin, Timer, ADC
led = Pin(25, Pin.OUT)
temperature = ADC(4)
timer = Timer()

def blink(timer):

timer.init(freq=2, mode=Timer.PERIODIC, callback=blink)

The on-board LED will flash at 1 Hz, and the console will report the ADC value on the temperature channel.

You can use PicoReg to display the raw ADC value, in the ADC.RESULT register:

The state of the I/O pins is in the SIO.SPIO_IN register:

You can even see activity on the USB link, as the data pins toggle high and low:

Potential issues


This proved to be a surprising headache; it has been remarkably difficult to get consistent styling. Some of the screenshots were taken on a Pi 3 running Qt 5.11.3, remote-controlled from a PC, using SSH:

export DISPLAY=:0.0
python3 picoreg_qt.py

The others are run directly from a Pi console, and look quite different (and not as nice, in my opinion).

I’ve already used some very limited styling to customise the tree display:

TREE_STYLE    = ("QTreeView{selection-color:#FF0000;} " +
                 "QTreeView{selection-background-color:#FFEEEE;} ")


However, after many hours of experimentation, I still haven’t found a way of getting a consistent appearance – feel free to tackle this problem yourself!


PicoReg uses pure-python code; on the plus side, you get the convenience of not having to install any device-drivers, but on the minus-side the SWD timing is a bit unpredictable, and can be stretched out when a higher-priority task takes control of the CPU. This is particularly noticeable when resizing or repositioning the display window; the software makes 3 attempts to re-establish connection with the Pico, but may time out and disconnect.

This behaviour is harmless, and it is only necessary to click the ‘Connect’ button to re-establish communications.

Console interface

If there are problems running the graphical interface, the low-level drivers in can be run directly from the command line:

python3 picoreg_gpio.py [options] [address]
    options: -v Verbose mode
             -r Repeated access 
    address: hexadecimal address to be monitored

The default address to be monitored is the GPIO input 0xD0000004. The low-level driver can’t access the SVD database, so the address has to be specified in hexadecimal, e.g. to display the timer value:

python3 picoreg_gpio.py -r 0x4005400c
SWD connection restart
DPIDR 0x0bc12477
0x4005400c: 0xefa1b0d0
0x4005400c: 0xefa26046
0x4005400c: 0xefa30fc8
0x4005400c: 0xefa3bf32
0x4005400c: 0xefa46ec3
..and so on, use ctrl-C to exit

How it works

The SWD link has 2 wires: a clock line, and a bi-directional data line. All transactions are initiated by the Pi, the Pico only transmits on the data line when requested. The data is sent LSB (Least Significant Bit) first.

Each message starts with an 8-bit header, and the Pico responds with a 3-bit acknowledgement; if this is OK (bit value 1, 0, 0) then a 32-bit data value is transferred (sent or received), followed by a parity bit.

As there is only one data line, the Pi has to switch its direction from transmit to receive to get the acknowledgement, and then switch back to transmit (if it is a write cycle) or allow the Pico to send 32 bits of data (if it is a read cycle).

An extra complication that isn’t emphasised in the ARM documentation, is that the active edge changes as well; the Pi sends data that is stable on the rising clock edge, but the Pico data is stable on the falling clock edge, as shown in the following oscilloscope trace. [The response from the Pico has been amplified for clarity; normally it is the same magnitude as the outgoing signal from the Pi.]

SWD header transfer; blue trace is clock (1 MHz), red trace is data

The unusual elements of this protocol make it quite tricky to implement in pure Python, and I must admit the above trace wasn’t generated by my code; it comes from OpenOCD, with an FTDI USB adaptor. The following trace is generated by my code, the frequency is a bit lower (approximately 100 kHz) and is less symmetrical.

SWD header, using Python GPIO

The asymmetric waveform isn’t a problem, but a disadvantage of the pure-Python approach is that there are occasions when the CPU is performing other tasks, and it stop driving the SWD interface.

The trace below shows a 300 microsecond pause in the middle of a transfer, and unsurprisingly the Pico doesn’t like this, and returns an error response. Occasional errors like this aren’t a problem, as they are easily handled by the retry mechanism in the PicoReg code.

SWD transfer with gap in transmission

Error handling

When there is an error, the Pico completely stops communicating over the SWD interface, and ignores all subsequent commands; it is necessary to reset the SWD interface, to re-establish communication.

The reset process is deliberately quite complex, involving the transmission of:

  • 8 all-1 bits (FF hex)
  • 16 bytes of a specific polynomial
  • 4 all-0 bits (0 hex)
  • 1 byte to activate the SW-DP interface
  • At least 50 all-1 bits

If there are any errors in the process, the Pico will not respond, which makes debugging a bit tricky. Once reset, the connection has to re-established by, transmitting the ID of the core to be accessed; the RP2040 processor has two cores, that are selected using a value of 0x01002927 or 0x11002927. When browsing the peripheral registers, it doesn’t matter which core is selected; the results will be the same.

Higher-level protocol

You might think that, having established contact with the CPU, it would be easy to read the register values, but in reality the interface has several extra levels of complication; some of these I’ve already documented in a previous project, but if you are seeking more information, you really need to read the Arm Debug Interface Architecture Specification (ADI). At the time of writing the latest version (v6.0) is available here.


Remote logic waveform display using WebGL

Logic analyser display using WebGL

In a previous post, I used hardware-accelerated graphics to display oscilloscope waveforms in a browser; the next step is to plot logic waveforms, as would normally be displayed on a logic analyser.

We could potentially use exactly the same techniques as before, but this becomes quite inefficient when displaying a large number of logic channels. A real-time display needs a fast redraw-rate, which means maximising the work done by the Graphics Processor (GPU), since it can perform parallel calculations much faster than the Central Processor (CPU), even when running on a low-cost PC, tablet or mobile phone.

Looking at the above graphic, you can see that a major computation step is converting each 16-bit sample value into 16 ‘high’ or ‘low’ trace values. The trace has 20,000 samples, so 320,000 such calculations are needed, which is still quite do-able on the CPU, but it is not unusual for the sample count to be a million or more, which can result in a very sluggish display, making it difficult to scroll through the data, when looking for a specific data pattern.

Fortunately WebGL version 2 supports a technique called ‘instanced rendering’, which we can use to generate 16 or more trace values from a single data word. So we can feed the GPU with a block of raw sample data, and it will split that word into bits, and draw the appropriate vectors on the display, applying the necessary scaling, offsets and colouring.

Shader programming

Graphics Processor (GPU) programming is generally known as shader programming because the main computational elements are the ‘vertex’ and ‘fragment’ shaders, that take in a stream of data, and output a stream of pixels.

To optimise the shader operation, all the attributes for all the lines are usually stored in a single buffer, which is passed to the shaders as a single block; this allows the shaders to work at maximum speed, without having to interact with the main CPU.

In addition to this data, there are ‘uniform’ variables, that allow slowly-changing data values to be passed from the CPU to the GPU, for example specifying the scale, offset and colour of the individual traces.

Instanced rendering

Traditionally there is a one-to-one relationship between the incoming vertex attributes, and an object plotted on the screen; for example, the attributes may specify a line in 3 dimensions (2 points, each with z, y and z dimensions) and the shaders will draw that line on the display, by colouring in the necessary pixels.

Instanced rendering (in WebGL v2) allows a single set of vertex attributes to generate several copies (‘instances’) of an object. For example, one set of attributes can generate 100 identical objects, which is much more efficient than using adding 100 sets of attributes to the buffer.

At first sight, it is difficult to understand how this can be of use; the instances share the same coordinate values, so won’t they all be plotted on top of each other? The answer is yes, they will all be at the same location, unless we use the ‘instance number’ provided by the shader to space them out. So if I’m creating 16 instances from one sample value, they will be numbered 0 to 15. This allows me to assign a specific y-value to each instance, ensuring they are stacked in the display without overlap – and I can also use the instance number as a mask to select the appropriate bit-value to be plotted:

// Simplified vertex shader code

// Array with input data
in float a_data;

// Array with y scale & offset values
uniform vec2 u_scoffs[MAX_CHANS];

// Number of data points
uniform int u_npoints;

// Main shader operation
void main(void) {
    // Convert sample value from float to integer
    int d = int(a_data);

    // Get data bit for this instance (this trace)
    bool hi = (d & (1<<gl_InstanceID)) != 0;

    // Get x-position from vertex num, normalised to +/- 1.0
    float x = float(gl_VertexID) * (2.0/float(u_npoints)) - 1.0;

    // Get y-position from scale & offset array
    float y = u_scoffs[gl_InstanceID][1];

    // Adjust y-value if bit is high
    if (hi)
        y += u_scoffs[gl_InstanceID][0];

    // Set xyz position of this point

    // Set colour of this point (red if high)
    v_colour = hi ? vec4(0.8, 0.2, 0.2, 1.0) : vec4(0.3, 0.3, 0.3, 1.0);

A notable feature is that I’ve specified the input data as being 32-bit floating-point numbers, when the incoming data samples are really 16-bit integers. This is because I had difficulty in persuading the shader code to compile with 16 or 32-bit integer inputs; only floating-point numbers seem to be acceptable for the vertex attributes. There is evidence on the Web to suggest that this is a feature of the shader hardware, and not a shortcoming of the compiler, but regardless of the reason, I’m doing the conversion to and from floating-point values, until I can be sure that integer attributes won’t cause problems.

The Javascript command to draw the traces is:

gl.drawArraysInstanced(gl.LINE_STRIP, 0, trace_data.length, num_traces);

The lines are plotted in ‘strip’ mode, so plotting 20,000 line segments requires 20,001 coordinate points, as the end of one line segment is the same as the start of the next line. This has the consequence of making all the rising and falling edges non-vertical, as seen below.

Zoomed-in analyser trace

This display is highly unconventional; logic analysers normally only draw vertical & horizontal lines. However, I quite like this feature, since it acts as a reminder of the limitations due to the sample rate; if a pulse is shown as a triangle, it only changed state for one sample-period.


We need a way of adding annotation to the WebGL display, for example the trace identification boxes on the left-hand side. It is possible to draw these using WebGL, but the process is a bit complex; it is much easier to just overlay a second canvas with a 2D rendering context, and use that API to superimpose lines & text onto the WebGL image, e.g.

      .container {
         position: relative;
      #graph_canvas {
         position: relative;
      #text_canvas {
         position: absolute;
         left: 0; top: 0; z-index: 10;
     <div class="container">
         <canvas id="graph_canvas"></canvas>
         <canvas id="text_canvas"></canvas>

The text canvas will remain transparent until we draw on it, for example:

// Get 2D context
var text_ctx = text_canvas.getContext("2d");

// Clear the text canvas
function text_clear() {
    text_ctx.clearRect(0, 0, text_ctx.canvas.width, text_ctx.canvas.height);
// Draw a text box, given bottom-left corner & size
function text_box(x, y, w, h, txt) {
    text_ctx.font = h + 'px sans-serif';
    text_ctx.textBaseline = "bottom";
    text_ctx.rect(x-1, y-h-1, w+2, h);
    text_ctx.fillText(txt, x, y, w);


An analyser trace may have a million or more 16-bit samples, so we need the ability to magnify an area of interest. In theory I could extract the relevant section of the data, and feed it to the shader as a smaller buffer, but the main thrust of this project is to let the shader do all the hard work, so I always give it the full set of samples, and a two ‘uniform’ variables are used to specify an offset into the data, and the number of samples to be displayed.

float x = float(gl_VertexID-u_disp_oset) * (2.0/float(u_disp_nsamp)) - 1.0;
float y = u_scoffs[gl_InstanceID][1];
if (hi)
    y += u_scoffs[gl_InstanceID][0];

To calculate the x-position, the VertexID is used, which is an incrementing counter that keeps track of how many samples have been processed; this is combined with disp_oset and disp_nsamp which are the offset (in samples) of the start of the data to be displayed, and the number of samples to be displayed. The resulting x-value is normalised, i.e. scaled to within -1.0 and +1.0 for all the pixels that should be displayed; any pixels outside that range will be suppressed.

The y-position of a ‘0’ bit is derived from the offset array for each channel; for a ‘1’ bit, a scale value is added on. This allows the main Javascript code to control the position & amplitude of each trace, ensuring they don’t overlap.

16-channel display

Number of channels

I’ve imposed an arbitrary maximum of 16 channels (i.e. 16 traces in the display); the GPU is capable of plotting many more than that, but there is a limitation due to the fact that we’re sending floating-point values to the vertex shader; this has a 24-bit mantissa, so if we feed in values with 24 or more bits, there is the risk that the floating-point value will be an approximation of the sample data, which is useless for this application, since the precise value of each bit is important. So take care when increasing the number of channels beyond 16; make sure the data being plotted is the same as the data being gathered.

The web page includes selection boxes to set the number of channels, and zoom level. The Javascript code automatically adjusts the scale & offset values for each channel; you can resize the window to anything convenient, and they will automatically fill the given canvas size.

8 channel display with zoom

Browser compatibility

The code should run on relatively modern browsers that support WebGL v2, regardless of operating system; I have had success running Chrome 87 and Firefox 61 on Windows 7 & 10, Linux and Android.

With regard to Apple products, older browsers such as Safari 10 don’t work at all, and I’ve had no success with an iPhone 8. However, Safari 14 does work if you specifically enable WebGL 2 in the developer ‘experimental features’, and Chrome on a Macbook Pro runs fine.

Running the code

There is a single HTML file webgl_logic.html, which has all the page formatting, Javascript, and GLSL v2 shader code; it is available on Github here.

When loaded into a suitable browser, it will display 16 waveforms, representing 20,000 samples with a 16-bit binary value incrementing from zero. The display can be animated once by clicking ‘single’, or continuously using ‘run’. You can adjust the zoom level at any time by clicking the selection box, or pressing ‘+’ or ‘-‘ on the keyboard. The offset of the zoomed area is modified by clicking on the display area, then using the left and right-arrow keys.

To experiment with larger data sets, you can increase the NSAMP value in the Javascript code. Don’t forget that by default every data sample generates 16 traces, so 1 million samples means the GPU will be generating 16 million vectors; the speed with which it can do this will depend on the specification of your hardware, and size of the display window, but you can get reasonable performance on a typical office PC; an expensive graphics card isn’t necessary.


Remote oscilloscope display using WebGL

Using WebGL for a remote oscilloscope display



If you want to display logic waveforms instead of analogue traces, see my other post.

WebGL programming





WebGL vertex processing





WebGL versions



// GLSL v2 definitions:
  in vec3 a_coords;
  out vec4 v_colour;

// GLSL v1 definitions:
  attribute vec3 a_coords;
  varying vec4 v_colour;


Shader programming


vert_code = `#version 300 es
    #define MAX_CHANS ${MAX_CHANS}
    in vec3 a_coords;
    out vec4 v_colour;
    uniform vec4 u_colours[MAX_CHANS];
    uniform vec2 u_scoffs[MAX_CHANS];
    vec2 scoff;
    void main(void) {
        int zint = int(a_coords.z);
        scoff = u_scoffs[zint];
        v_colour = u_colours[zint];





frag_code = `#version 300 es
    precision mediump float;
    in vec4 v_colour;
    out vec4 o_colour;
    void main() {
        o_colour = v_colour;


// Compile a shader
function compile_shader(typ, source) {
    var s = gl.createShader(typ);
    gl.shaderSource(s, source);
    if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
        throw "Could not compile " +





  <canvas id = "graph_canvas"></canvas>


Data acquisition



0.000,1.000,0.001,1.000,0.000,1.001 ..and so on until..<LF>


    // Decode CSV string into floating-point array
    function csv_decode(s) {
        return data.map(x => parseFloat(x));



Web server


# Oscilloscope-type ADC data display
class Grapher(object):

    # Index: show oscilloscope display
    def index(self):
        return cherrypy.lib.static.serve_file(directory + "/webgl_graph.html")

if __name__ == '__main__':
    conf = {
        '/': {
            'tools.staticdir.root': os.path.abspath(os.getcwd())
    cherrypy.quickstart(Grapher(), '/', conf)



Graph page loaded from Web server


    def sim(self):
        global nresults
        data = npoints * [0]
        for c in range(0, npoints, nchans):
            if nchans > 1:
        nresults += 1
        return rsp


Display of simulated data


    # FIFO data source
    def fifo(self):
            f = open(fifo_name, "r")
            rsp = f.readline()
            rsp = "No data"
        return rsp

Running the code

Install the cherrypy server using:

# For Python 2
pip install cherrypy
# ..or for Python 3 on the Raspberry Pi
pip3 install cherrypy






Display of 2 ADC channels



Fast oscilloscope display using OpenGL on the Raspberry Pi








Shader operation





OpenGL versions



Shader programming



#version 300 es
precision mediump float;
in vec3 coord3d;
flat out vec4 f_color;


#define SL(s) s "\n"
char frag_shader[] =
    SL("#version 300 es")
    SL("precision mediump float;")
    SL("in vec3 coord3d;")
    SL("flat out vec4 f_color;")
    ..and so on until..


#define MAX_TRACES 17


// In the main program..
#define VALSTR(s) #s

// In the GLSL code string..

The rest of the vertex shader program string looks like this:

    SL("uniform vec4 u_colours[MAX_TRACES];")
    SL("uniform vec2 u_scoffs[MAX_TRACES];")
    SL("vec2 scoff;")
    SL("int zint;")
    SL("bool zen;")
    SL("void main(void) {")
    SL("    zint = int(coord3d.z);")
    SL("    scoff = u_scoffs[zint];")


The fragment shader doesn’t do much; it just copies the colour value:

char frag_shader[] =
    SL("#version 300 es")
    SL("precision mediump float;")
    SL("flat in vec4 f_color;")
    SL("void main(void) {")
    SL("    fragColor = f_color;")


Main program

Pi 3 OpenGL oscilloscope display, 1000 samples





typedef struct {
    GLfloat x;
    GLfloat y;
    GLfloat z;

// Set x, y and z values for single point
    pp->x = x;
    pp->y = y;
    pp->z = z;

// Move, then draw line between 2 points

Building the software


sudo apt update
sudo apt install freeglut3-dev libglew-dev




  • LINE_WIDTH: width of plot line (2)


There are also some command-line options:

-v              Verbose display for debugging

-display  <val> Standard X display selector
-geometry <val> Standard X display resolution and position



Running the application



./rpi_opengl_graph -display :0.0



./rpi_opengl_graph -geometry 640x480



In one console:
In a second console:
  ./rpi_opengl_graph -geometry 1024x768 -i 1 -n 1000



# Simple simulation of ADC feeding Linux FIFO

import math, time, os, signal, sys, random

fifo_name = "/tmp/adc.fifo"
ymax = 2.0
delay = 0.02
nchans = 2
npoints = 1000
running = True
fifo_fd = None

def remove(fname):
    if os.path.exists(fname):

def shutdown(sig=None, frame=None):
    if fifo_fd:

data = npoints * [0]
n = 0;
signal.signal(signal.SIGINT, shutdown)
    f = open(fifo_name, "w")
    running = False
while running:
    for c in range(0, npoints, nchans):
        if nchans > 1:
    n += 1
        f.write(s + "\n")
        running = False


./rpi_opengl_graph -geometry 640x480


rpi_opengl_graph display with adc_sim input


Streaming analog data from a Raspberry Pi




ADC hardware

AB Electronics ADC DAC Zero on a Pi 3B



ADC serial protocol


  Start bit:                 1
  Single-ended mode          1
  Channel number             0 or 1
  M.S. bit first             1

  Undefined bits (floating)  x x x x
  Null bit                   0


ADC readings on a Pi Zero


Raspberry Pi SPI

The SPI controller has the following 32-bit registers:

  • LTOH (LOSSI output hold delay): not used











SPI, PWM and DMA initialisation


Version   PWM   SPI   REG_BASE     DMA channels used by OS
0         250   400   0x20000000   0, 2, 4, 6
1         250   250   0x20000000   0, 2, 4, 6
2         250   250   0x3F000000   0, 2, 4, 6
3         250   250   0x3F000000   0, 2, 4, 6




Using DMA



  1. Prepare the CBs and data in user memory.
  2. Copy the CBs and data across to uncached memory
  3. Start the DMA controllers
  4. Start the DMA pacing


typedef struct {
    DMA_CB cbs[NUM_CBS];
    uint32_t samp_size, pwm_val, adc_csd, txd[2];

    ADC_DMA_DATA *dp=mp->virt;
    ADC_DMA_DATA dma_data = {
        .cbs = {
        dma_data.cbs[2].next_cb = 0;


The format of a control block is:

  • Source address
  • Destination address
  • Transfer length (in bytes)
  • Stride: skip unused values (not used)
  • Next Control Block: zero if last block
  • Debug: additional diagnostics




Transfer info:       wait for data request from SPI receiver
Source address:      microsecond counter register
Destination address: memory
Transfer length:     4 bytes
Stride:              not used
Next control block:  CB 1
Debug:               not used



Starting DMA


    *REG32(pwm_regs, PWM_DMAC) = PWM_DMAC_ENAB | PWM_ENAB;


// Start ADC data acquisition
void adc_stream_start(void)



Compiling and running the code



gcc -Wall -o rpi_disp_dma rpi_disp_dma.c rpi_dma_utils.c
gcc -Wall -o rpi_adc_stream rpi_adc_stream.c rpi_dma_utils.c



DMA channel scan

The DMA scan is run as follows:

  sudo ./rpi_disp_dma
Response (Pi ZeroW):
  DMA channels in use: 0 2 4 6



ADC streaming


  sudo ./rpi_adc_stream -t
  RPi ADC streamer v0.20
  VC mem handle 5, phys 0xde50f000, virt 0xb6f5f000
  Testing 1.000 MHz SPI frequency:   1.000 MHz
  Testing   100 Hz  PWM frequency: 100.000 Hz


The command-line options are case-insensitive:

-T          Test mode
-V          Verbose mode. Enable hexadecimal data display.


  sudo ./rpi_adc_stream
  RPi ADC streamer v0.20
  VC mem handle 5, phys 0xde50f000, virt 0xb6fd1000
  SPI frequency 1000000 Hz
  ADC value 686 = 1.105V



  RPi ADC streamer v0.20
  VC mem handle 5, phys 0xde50f000, virt 0xb6f7e000
  Created FIFO '/tmp/adc.fifo'
  Streaming 10 samples per block at 20 S/s


  cat /tmp/adc.fifo
Response (with 1.1V on ADC 'IN1'):




Command in console 1:
  Streaming 1 samples per block at 10 S/s

Command in console 2:
  cat /tmp/adc.fifo
Response in console 2 (with 1.1 volt input):
  ..and so on, at 10 lines per second



Command in console 1:
Response in console 1:
  Streaming 2 samples per block at 10 S/s
Response when streaming starts:
  Started streaming to FIFO '/tmp/adc.fifo'
  F2 AD 00 00 F0 01 00 00
  F2 AE 00 00 F0 01 00 00
  F2 AE 00 00 F0 01 00 00
  F2 AE 00 00 F0 00 00 00
  ..and so on..

Command in console 2:
  cat /tmp/adc.fifo
  ..and so on..

Displaying streaming data



Trend display






sudo apt install trend


Termeter display



It is installed using:

cd ~
sudo apt install golang
go get github.com/atsaki/termeter/cmd/termeter



Then the display is started in a second console:



Plotting in Python

Python plot of streaming data




import numpy as np
from matplotlib import pyplot, animation

fifo_name = "/tmp/adc.fifo"
npoints  = 100
interval = 500
xlim     = (0, 1)
ylim     = (0, 3.5)

fifo = open(fifo_name, "r")
fig = pyplot.figure()
ax = pyplot.axes(xlim=xlim, ylim=ylim)

def init():
    line.set_data([], [])
    return line,

def animate(i):
    x = np.linspace(0, 1, npoints)
    line.set_data(x, y)
    return line,

anim = animation.FuncAnimation(fig, animate, init_func=init,
                               frames=npoints, interval=interval, blit=True)





WebGL oscilloscope display



Raspberry Pi 16-channel WS2812 NeoPixel LED driver

Raspberry Pi driving smart LEDs




Pulse shape


LED pulse sequence


LED pulses





Generating the pulses



SMI pins on I/O header




// Data for simple transmission test


Binary test waveform: 8-bit SMI


// Swap adjacent bytes in transmit data
void swap_bytes(void *data, int len)
    uint16_t *wp = (uint16_t *)data;
    len = (len + 1) / 2;
    while (len-- > 0)
        *wp = __builtin_bswap16(*wp);

The resulting waveform is correct:

Corrected test waveform




SMI I/O lines




Correct (lower) and incorrect (upper) drive waveforms







           Ch7 Ch6 Ch5 Ch4 Ch3 Ch2 Ch1 Ch0
            1   1   1   1   1   1   1   1
Grn bit 7:  x   x   x   x   x   x   x   x
            0   0   0   0   0   0   0   0
            1   1   1   1   1   1   1   1
Grn bit 6:  x   x   x   x   x   x   x   x
            0   0   0   0   0   0   0   0
..and so on until..
            1   1   1   1   1   1   1   1
Grn bit 0:  x   x   x   x   x   x   x   x
            0   0   0   0   0   0   0   0
            1   1   1   1   1   1   1   1
Red bit 7:  x   x   x   x   x   x   x   x
            0   0   0   0   0   0   0   0
..and so on until..
            1   1   1   1   1   1   1   1
Blu bit 0:  x   x   x   x   x   x   x   x
            0   0   0   0   0   0   0   0



#if LED_NCHANS > 8
#define TXDATA_T        uint16_t
#define TXDATA_T        uint8_t

void rgb_txdata(int *rgbs, TXDATA_T *txd)
    int i, n, msk;

        // Mask to convert RGB to GRB, M.S bit first
        // 1st byte is a high pulse on all lines
        txd[0] = (TXDATA_T)0xffff;
        // 2nd byte has high or low bits from data
        // 3rd byte is low pulse
        txd[1] = txd[2] = 0;
            if (rgbs[i] & msk)
        txd += BIT_NPULSES;

Beware caching




Copy data into transmit buffer
Byte-swap the buffer data
Transmit the buffer data




Prepare data in transmit buffer
Byte-swap the buffer data
Transmit the buffer data


Prepare data in local buffer
Byte-swap the local data
Copy local data to transmit buffer
Output the transmit buffer data

Source code



// Location of peripheral registers in physical memory
#define PI_01_REG_BASE  0x20000000  // Pi Zero or 1
#define PI_23_REG_BASE  0x3F000000  // Pi 2 or 3
#define PI_4_REG_BASE   0xFE000000  // Pi 4

The application is compiled and run using:

gcc -Wall -o rpi_pixleds rpi_pixleds.c rpi_dma_utils.c

sudo ./rpi_pixleds [options] [RGB_values]

The options can be in upper or lower case:

-n num    # Set number of LEDs per channel
-t        # Set test mode


sudo ./rpi_pixleds -n 5 -t


sudo ./rpi_pixleds -n 5

sudo ./rpi_pixleds -n 3 0 0 ff0000

sudo ./rpi_pixleds 7f0000 007f00 00007F

sudo ./rpi_pixleds  ff0000,ff2020 00ff00,20ff20 0000ff,2020ff


Compile-time options


#define LED_D0_PIN      8   // GPIO pin for D0 output
#define LED_NBITS       24  // Number of data bits per LED
#define CHAN_MAXLEDS    50  // Maximum number of LEDs per channel
#define CHASE_MSEC      100 // Delay time for chaser light test

The main items that might be changed are:


Possible problems




/usr/bin/tvservice -o




Raspberry Pi Secondary Memory Interface (SMI)

Colour video signal captured at 25 MS/s




Parallel interface

Raspberry Pi SMI signals



The SMI functions are assigned to specific GPIO pins:


Parallel DAC






#define DAC_D0_PIN      8
#define DAC_NPINS       8

extern MEM_MAP gpio_regs;

// Output value to resistor DAC (without SMI)
void dac_ladder_write(int val)

// Initialise resistor DAC
void dac_ladder_init(void)
    int i;
        gpio_mode(DAC_D0_PIN+i, GPIO_OUT);

// Output sawtooth waveform
while (1)
    i = (i + 1) % 256;



SMI register definitions




#define SMI_CS_FIELDS \

volatile SMI_CS_REG  *smi_cs;




#define STRS_(...)  #__VA_ARGS__

char *smi_cs_regstrs = STRS(SMI_CS_FIELDS);

// Display bit values in register
    uint32_t nbits, v;
        p = q + 1;
        nbits = 0;
        val >>= nbits;
            p = r = p + 1;



..which produces a display like..


SMI registers

The SMI registers are:

CS:  control and status
L:   data length (number of transfers)
A:   address and device number
D:   data FIFO
DMC: DMA control
DSR: device settings for read
DSW: device settings for write
DCS: direct control and status
DCA: direct control address and device number
DCD: direct control data



Some notable fields in the control & status register are:





Direct Mode


    • The width of the strobe pulse, that triggers the transfer




// Width values
#define SMI_8_BITS  0
#define SMI_16_BITS 1
#define SMI_18_BITS 2
#define SMI_9_BITS  3

    int divi = ns/2;

    if (smi_cs->seterr)
        smi_cs->seterr = 1;
    smi_dsr->rsetup = smi_dsw->wsetup = setup; 
    smi_dsr->rstrobe = smi_dsw->wstrobe = strobe;
    smi_dsr->rhold = smi_dsw->whold = hold;
    smi_dsr->rwidth = smi_dsw->wwidth = width;




// Initialise resistor DAC
void dac_ladder_init(void)
    smi_cs->clear = 1;
    smi_cs->aferr = 1;
    smi_dcs->enable = 1;

// Output value to resistor DAC
void dac_ladder_write(int val)
    smi_dcs->done = 1;
    smi_dcs->write = 1;
    smi_dcd->value = val & 0xff;
    smi_dcs->start = 1;







Oscilloscope trace of DMA passthrough (200 ns/div)




#define NCYCLES 4

// DMA values to resistor DAC
    DMA_CB *cbs=mp->virt;
    memcpy(txdata, data, len);
    cbs[0].tfr_len = NSAMPLES * NCYCLES;

smi_dsr->rwidth = SMI_8_BITS; 
smi_l->len = NSAMPLES * REPEATS;
smi_cs->pxldat = 1;
smi_dmc->dmaen = 1;
smi_cs->write = 1;
smi_cs->enable = 1;
smi_cs->clear = 1;
smi_cs->start = 1;



Oscilloscope trace of DAC output (200 us/div)


Read cycle test


smi_cs->write = 1;





AD9226 ADC





H/W pin	Function	AD9226
16	GPIO23 SD15	D0 (MSB)
15	GPIO22 SD14	D1	
40	GPIO21 SD13	D2	
38	GPIO20 SD12	D3	
35	GPIO19 SD11	D4	
12	GPIO18 SD10	D5	
11	GPIO17 SD9	D6	
36	GPIO16 SD8	D7	
10	GPIO15 SD7	D8	
8	GPIO14 SD6	D9	
33	GPIO13 SD5	D10
32	GPIO12 SD4	D11 (LSB)
2	5V		+5V

Direct mode




    gpio_mode(ADC_D0_PIN+i, GPIO_IN);
gpio_mode(SMI_SOE_PIN, GPIO_ALT1);


smi_start(10, 1);
val = adc_gpio_val();

Voltage value


#define ADC_ZERO        2080
#define ADC_SCALE       410.0

// Convert ADC value to voltage
float val_volts(int val)
    return((ADC_ZERO - val) / ADC_SCALE);

// Return ADC value, using GPIO inputs
int adc_gpio_val(void)
    int v = *REG32(gpio_regs, GPIO_LEV0);




Avoiding bus conflicts





ADC code


// Get GPIO mode value into 32-bit word
    uint32_t mask = 7 << (n * 3);

// Start DMA for SMI ADC, return Rx data buffer
uint32_t *adc_dma_start(MEM_MAP *mp, int nsamp)
    DMA_CB *cbs=mp->virt;

    // Get current mode register values
    // Get mode values with ADC pins set to SMI
    // Copy mode values into 32-bit words
    *modep1 = modes[1];
    *modep2 = modes[2];
    *pindata = 1 << TEST_PIN;
    cbs[0].tfr_len = 4;
    cbs[1].tfr_len = 4;
    // Control block 2: read data
    // Control block 3: disable SMI I/P pins
    cbs[3].ti = DMA_CB_SRCE_INC | DMA_CB_DEST_INC;
    cbs[3].tfr_len = 3 * 4;


// ADC DMA is complete, get data
    uint16_t *bp = (uint16_t *)buff;
    int i;
        if (i >= PRE_SAMP)

ADC speed tests


// RPi v0-3
#define SMI_NUM_BITS    SMI_16_BITS
#define SMI_TIMING      SMI_TIMING_25M

init_smi(SMI_16_BITS,  SMI_TIMING);


RPi v3

25 MS/s capture of a video test waveform


Scope trace 500 ns/div, 2 volts/div



Scope trace 5 us/div, 2 volts/div


Scope trace 200 ns/div, 2 volts/div



#define SMI_NUM_BITS    SMI_8_BITS
#define SMI_TIMING      SMI_TIMING_50M
#define SAMPLE_SIZE     1


Pi ZeroW


Capture of 500 kHz triangle wave



RPi v4


// RPi v4



Scope trace 5 usec/div, 2 volts/div




Running the code



// Location of peripheral registers in physical memory
#define PI_01_REG_BASE  0x20000000  // Pi Zero or 1
#define PI_23_REG_BASE  0x3F000000  // Pi 2 or 3
#define PI_4_REG_BASE   0xFE000000  // Pi 4


gcc -Wall -pedantic -o rpi_smi_adc rpi_smi_adc.c rpi_dma_utils.c


sudo ./rpi_smi_adc
sudo ./rpi_smi_adc > test6.csv




dtoverlay=smi    # Not needed!




Fast data capture with the Raspberry Pi

Video signal captured at 2.6 megasamples per second




Direct Memory Access




// Structure for mapped peripheral or memory
typedef struct {
    int fd,         // File descriptor
        h,          // Memory handle
        size;       // Memory size
    void *bus,      // Bus address
        *virt,      // Virtual address
        *phys;      // Physical address


#define SPI0_BASE       (PHYS_REG_BASE + 0x204000)

// Use mmap to obtain virtual address, given physical
    mp->phys = phys;
    mp->size = PAGE_ROUNDUP(size);
    mp->bus = phys - PHYS_REG_BASE + BUS_REG_BASE;

MEM_MAP spi_regs;

Then a macro is used to access a specific register:

#define SPI_DLEN        0x0c

*REG32(spi_regs, SPI_DLEN) = 0;


*REG32(spi_regs, SPI_CS) |= 1;


Analog-to-Digital Converters (ADCs)


  1. Inter-Integrated Circuit (I2C) serial bus
  2. Serial Peripheral Interface (SPI) serial bus
  3. Parallel bus






The RasPiO Analog Zero board ( https://rasp.io/analogzero/ ) has the Microchip MCP3008 ADC on it, and very little else.


SPI interface


  • GPIO8 CE0: SPI 0 Chip Enable 0
  • GPIO11 SCLK: Clock signal
  • GPIO10 MOSI: data output to ADC
  • GPIO9 MISO: data input from ADC



from gpiozero import MCP3008
adc = MCP3008(channel=0)
print(adc.value * 3.3)


import spidev
spi = spidev.SpiDev()
spi.open(0, 0)
spi.max_speed_hz = 500000
spi.mode = 0
msg = [0x01,0x80,0x00]
rsp = spi.xfer2(msg)


RPi SPI access of an MCP3008 ADC



MCP3008 software

Here is the C equivalent of the Python code:

// Set / clear SPI chip select
void spi_cs(int set)
    uint32_t csval = *REG32(spi_regs, SPI_CS);


// Transfer SPI bytes
    while (len--)

// Fetch single 10-bit sample from ADC
int adc_get_sample(int chan)

    spi_xfer(txdata, rxdata, sizeof(txdata));


// Fetch 9-bit sample from ADC
int adc_get_sample(int chan)
    spi_xfer(txdata, rxdata, sizeof(txdata));



// Fetch single sample from MCP3008 ADC using DMA
int adc_dma_sample_mcp3008(MEM_MAP *mp, int chan)
    DMA_CB *cbs=mp->virt;

    dlen = 4;
    mcp3008_tx_data(&txd[1], chan);
    cbs[0].tfr_len = dlen;
    cbs[1].tfr_len = dlen + 4;
    *REG32(spi_regs, SPI_DLEN) = 0;

// Return Tx data for MCP3008
int mcp3008_tx_data(void *buff, int chan)
    memcpy(buff, txd, sizeof(txd));

// Return value from ADC Rx data
int mcp3008_rx_value(void *buff)
    uint8_t *rxd=buff;


// Wait until DMA is complete
void dma_wait(int chan)
    int n = 1000;

    do {
    if (n == 0)
        printf("DMA transfer timeout\n");



CB1: set chip select high
CB2: set chip select low
CB3: write next 32-bit word to the FIFO



    // Control block 0: read data from SPI FIFO
    cbs[0].tfr_len = dlen;
    // Control block 1: CS high
    // Control block 2: CS low
    // Control block 3: write data to Tx FIFO
    // Link CB1, CB2 and CB3 in endless loop



Raspberry Pi MCP3008 ADC input using DMA







ADS7884 on a prototyping board


The device pins are:

1  Supply (3.3V)
2  Ground
3  VIN (voltage to be measured)
4  SCLK (SPI clock)
5  SDO (SPI data output)
6  CS (chip select, active low)


This can be demonstrated by a Python program:

import spidev
bus, device = 0, 0
spi = spidev.SpiDev()
spi.open(bus, device)
spi.max_speed_hz = 1000000
spi.mode = 0
msg = [0x00,0x00]
res = spi.xfer2(msg)




So the connections between the ADC and RPi are:

Pin 1: 3.3V supply 
Pin 2: ground 
Pin 3: voltage to be measured
Pin 4: SPI0 clock, GPIO11
Pin 5: SPI0 MISO,  GPIO9
Pin 6: SPI0 MOSI,  GPIO10 (ADC chip select)


ADS7884 software


// Fetch samples from ADS7884 ADC using DMA
    DMA_CB *cbs=mp->virt;

    enable_dma(DMA_CHAN_A); // Enable DMA channels
    // Control block 0: store Rx data in buffer
    cbs[0].tfr_len = dlen;
    cbs[1].tfr_len = 4;
    cbs[2].tfr_len = 8;
    // DMA request every 4 bytes, panic if 8 bytes
    *REG32(spi_regs, SPI_DLEN) = 0;
    txd[1] = 0xffffffff;                // Set CS high
    txd[2] = 0x01000100;                // Pulse CS low
    // Enable DMA, wait until complete

There are a few points that need clarification:



Running the code

The software is in 3 files on Github here.




gcc -Wall -o rpi_adc_dma_test rpi_adc_dma_test.c rpi_dma_utils.c
sudo ./rpi_adc_dma_test



SPI ADC test v0.03
VC mem handle 5, phys 0xde510000, virt 0xb6f00000
SPI frequency 160000 Hz, 10000 sample/s
ADC value 212 = 0.683V

There are 2 command-line parameters:







Data display using gnuplot


