Updating Board Design

Choosing A Microcontroller

Since I have two different boards for the driver-side electronics and back-of-car electronics, I need to choose an appropriate microcontroller to design a second board with.

This forum revealed to me that the ESP32-C3 isn't suitable for controlling servo motors.

In hindsight, it's good that I designed my first board with a microcontroller that can use WiFi and Bluetooth because some part of the board will need to communicate the custom message to display on the screen, which can be updated remotely from an app or website. I might also want to consider handling this functionality on the other board, but I feel it's not necessary.

Prompt

I'm designing a PCB board to control a servo motor, an e-paper/e-ink LCD display, and receive signals  over bluetooth (or wifi - they won't be connected to internet so bluetooth is probably a better idea). I am trying to use a microcontroller. I originally was going to use the ESP32-C3 because of the built in bluetooth and wifi, but read some forum posts which revealed that it's not great at controlling a servo motor. I also want to use an integrated circuit and not an arduino or raspberry pi pico, but a board, for example, like the XIAO Seeed rp2040 or an ATTINYMEGA/ATTINY. What do you recommend? Also comment on any choices I've described to you and provide feedback for anything that's not a well-warranted decision or explain why my choices are logical.

Response

Your requirement to control a servo motor, an e-paper/e-ink LCD display, and receive signals over Bluetooth (or WiFi) can be achieved using various microcontroller options. Since you prefer not to use an Arduino or Raspberry Pi Pico, I will recommend a few alternatives.

STM32 microcontrollers: STM32 microcontrollers, particularly the STM32F series (e.g., STM32F103C8T6), offer a good balance of performance, power efficiency, and a variety of peripherals. They can handle servo motor control, e-paper/e-ink displays, and can be paired with a separate Bluetooth module (e.g., HC-05) for communication. You can find various STM32 development boards, such as the Blue Pill, that can be programmed using the Arduino IDE or other development environments.

nRF52 series microcontrollers: The nRF52832 and nRF52840 are ARM Cortex-M4-based microcontrollers with built-in Bluetooth Low Energy (BLE) capabilities. They have sufficient processing power and peripherals to control a servo motor and e-paper/e-ink displays. They are designed for low-power applications, which might be beneficial if power consumption is a concern.

ATSAMD21/51 microcontrollers: These microcontrollers from Microchip (previously Atmel) are ARM Cortex-M0+/M4 based and have good performance for controlling servo motors and e-paper/e-ink displays. You can pair them with an HC-05 Bluetooth module for communication. The Adafruit Feather M0/M4 boards use these microcontrollers, and you can find a variety of examples for controlling peripherals and communication.

Regarding your original choice of ESP32-C3, it is true that it might not be the best option for precise servo motor control due to the

Prompt

pick up where you left off

Response

Due to the fact that ESP32-C3 uses an RISC-V based core, it might not be the best option for precise servo motor control as it can have timing-related challenges. It is also designed with WiFi and Bluetooth connectivity as its primary focus, which can be overkill if you only require Bluetooth communication.

In summary, based on your requirements, I would recommend considering STM32 microcontrollers, nRF52 series microcontrollers, or ATSAMD21/51 microcontrollers as potential options for your PCB board design. These microcontrollers provide sufficient processing power and peripherals to handle servo motor control, e-paper/e-ink displays, and can be paired with a separate Bluetooth module for communication.

Ensure that you verify the specific peripherals and communication interfaces required for your chosen e-paper/e-ink display, servo motor, and Bluetooth module, and make sure the selected microcontroller supports those interfaces. Additionally, consider factors such as power consumption, form factor, and the availability of development resources, libraries, and community support when making your decision.

Prompt

This servo motor which moves the display needs to move the display onto the back window of a car, from the inside. How to you recommend I mount it?

Response

Mounting a servo motor to move a display inside a car can be done in a few steps. I will outline a general approach, but please note that the specific details may vary depending on the make and model of your car, the size and weight of the display, and the type of servo motor you are using.

Determine the best mounting location: Choose a location on the inside of the car's back window where the servo motor can be securely mounted and has sufficient space for the display to move. Make sure there are no obstructions and the window is not tinted in a way that would hinder the visibility of the display.

Design a mounting bracket: Create a custom mounting bracket that can securely hold the servo motor and attach it to the car's interior. The bracket should be made from a durable, lightweight material like aluminum or a strong plastic. The design should also consider cable management for the servo motor, display, and any other required connections.

Attach the mounting bracket: Attach the mounting bracket to the car's interior using strong adhesives, like automotive-grade double-sided tape or adhesive sealant, or by using screws or other fasteners if the car's design allows for it. Ensure that the mounting is secure and does not interfere with any other vehicle functions.

Attach the servo motor to the bracket: Secure the servo motor to the mounting bracket using screws, bolts, or other suitable fasteners. Make sure the servo motor is positioned correctly to move the display as desired.

5

It didn't finish its response for the last prompt, but that's fine since it advise was far to general to be helpful.

The responses for the microcontroller suggestion were helpful, though.

I took a look at Florian Becker and asked ChatGPT 4.0 somre more questions.

Prompt

which of the three suggested microcontrollers should i use

Response

Selecting the most suitable microcontroller from the three suggested options depends on your specific requirements, priorities, and design constraints. I will provide a brief comparison to help you decide:

STM32 microcontrollers: STM32 microcontrollers, particularly the STM32F series, offer a good balance of performance, power efficiency, and a variety of peripherals. They are widely used in the maker community, and you can find extensive resources, libraries, and examples for various applications. If you want a powerful, well-supported microcontroller and don't mind adding a separate Bluetooth module, the STM32 is a solid choice.

nRF52 series microcontrollers: The nRF52832 and nRF52840 are ARM Cortex-M4-based microcontrollers with built-in Bluetooth Low Energy (BLE) capabilities. They are particularly suitable for low-power applications, and their integrated BLE support simplifies the design. If power consumption is a concern or you prefer an integrated Bluetooth solution, the nRF52 series is an excellent option.

ATSAMD21/51 microcontrollers: These microcontrollers from Microchip (previously Atmel) are ARM Cortex-M0+/M4 based and have good performance for controlling peripherals. They are less powerful than STM32 microcontrollers but still provide sufficient resources for your application. If you are already familiar with Atmel microcontrollers or prefer a lower-cost solution, consider the ATSAMD21/51 microcontrollers

I decided on a STM32F series board and found this Instructables tutorial on how to wire and program an e-paper/e-ink display to the microcontroller. I also downloaded this EagleCAD library and imported it into Fusion 360.

I found this site which explains naming conventions for the STM32 boards.

STM32 Naming Convention

In the EagleCAD library I saw four boards:

  • STM32F10XCXT6
  • STM32F10XRXT6
  • STM32F10XTXU6
  • STM32F10XVXT6

I decided on the first board, STM32F10XCXT6, since it has the least number of pins (from what I could tell in the EagleCAD schematic) and is surface-mount where the pin headers are exposed (unlike the STM32F10XTXU6).

Hidden Headers

Pins Exposed

When I put the component in my schematic I at first only saw a small portion of the pins from one port. I then realized that if I kept added components the other pins would be added. I was quickly overwhelmed by the amount of boards and decided to switch to one with less pins.

STM32 Schematic

Based on the Instructable I decided to assign different pins on a 2x5 SMD pin header I found (PINHD-2X5-SMD in the fab library) to the following.

Header Pin STM32 Pin
1 SDA
2 SCL
3 CS
4 DC
5 RST
6 BU
7 BS

Wired Schematic STM32

Auto-Routed

I realized this chip has even more pins than an RP2040 (ATTINY328P) so I may as well just use that one. I could also use that XIAO Seeed RP2040 but am worried it won't have enough pins. This example confirms my worries.

XIAO Seeed to e-Paper

I instead decided to use an ATTINY328P-AN. The DigiKey site linked to this page where I could download a component that I thought I could import into EagleCAD (I selected the Eaglecad6 Library format in the DigiKey download). That format didn't work so I selected Fusion360 PCB which gave an LBR file, as needed.

After talking to Mr. Dubick, I decided to use a Raspberry Pi Pico which allows for me to more easily use an RP2040 with a lot of pins. I downloaded the EagleCAD library here. I used this of the three components since it was the entire Pico board (not just the RP2040 microprocessor) and is surface mount.

Pico Component

I also found this schematic for an e-paper HAT driver linked on this page from this website. I also found this post which compares different methods of operating the e-paper display, and I also skimmed this article.

This website confirmed that SPI communication can be achieved with the Raspberry Pi Pico, and this tutorial eloborates on how to implement it. I also learned that there's a specific HAT driver for a smaller e-paper display designed for the Pico, which is concerned as to whether it's possible to use the Pico to operate a larger display. This website suggested a potential wiring for Arduino to talk to the driver. Here is a detailed pinout sheet for the Pico and here is a detailed pinout sheet for the Arduino Uno.

I then watched this tutorial to gain a better understanding of e-paper displays. This revealed to me that the other wiring I got wasn't correct.

I wasn't sure whether my display was a 7.5", 7.5"B, or 7.5"C display, so I guessed 7.5" for now. I then downloaded a zip file of the code. I copied the epd7in5 folder in the 7.5in display folder to the Arduino Library folder (C:\Users\*YOUR_USER*\Documents\Arduino\libraries). Then it appeared in the Arduino IDE!

Epd Library Arduino IDE

I clicked in which added the following to the top of my code.

#include <epd7in5.h>
#include <epdif.h>

I then went to File > Examples > epd7in5 > epd7in5-demo but got an error that the path didn't exist. I then realized that this used a different address (which I accidentally put the library in before) so I wanted to copy the folder there, as well. But before I did that, I tried again, clicking the library under Examples from Custom Libraries and the example opened! I uploaded the code but got the following error.

:\Users\*USER*\AppData\Local\Temp\.arduinoIDE-unsaved2023223-2744-55l82c.2wca4\epd7in5-demo\imagedata.cpp:27:10: fatal error: avr/pgmspace.h: No such file or directory
 #include <avr/pgmspace.h>
          ^~~~~~~~~~~~~~~~
compilation terminated.

Using library SPI at version 2.0.0 in folder: C:\Users\*USER*\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.7\libraries\SPI 
Using library epd7in5 at version 1.0 in folder: C:\Users\*USER*\Documents\Arduino\libraries\epd7in5 
exit status 1

Compilation error: avr/pgmspace.h: No such file or directory

This site recommended that I simply commented out this include statement, so I changed the top of imagedata.cpp to the following.

#include "imagedata.h"
//#include <avr/pgmspace.h>

The same post later suggested that I change it to the following, and the compilation was successful, but I got an error that I forgot to select a port. I also changed the board to Arduino Uno.

#include "imagedata.h"
//#include <avr/pgmspace.h>
#if defined(AVR)
#include <avr/pgmspace.h>
#else  //defined(AVR)
#include <pgmspace.h>
#endif  //defined(AVR)

I then got an error that the sketch was too large.

Sketch too big; see https://support.arduino.cc/hc/en-us/articles/360013825179 for tips on reducing it.
Sketch uses 33522 bytes (103%) of program storage space. Maximum is 32256 bytes.
Global variables use 185 bytes (9%) of dynamic memory, leaving 1863 bytes for local variables. Maximum is 2048 bytes.
text section exceeds available space in board

Compilation error: text section exceeds available space in board

I then tried to use an Arduino Mega 2560 that had more memory. I was able to upload the sketch but nothing happened on the screen. I tried directly connecting the screen to the hat without the long ribbon cable and I tried the B/C and V2 versions of the examples in the Waveshare E-Paper library but nothing worked. I tried both the wiring configuration on this website and the one on this website none of which worked!

Failed Arduino Mega

I copied my documentation thus far into ChatGPT (excluding long parts at the end of some links to reduce the number of tokens) and asked it for advice. It's response wasn't helpful.

It seems like you have tried several different approaches to get the 7.5" e-paper waveshare display to work with your Raspberry Pi Pico, but haven't had success yet. Here are some suggestions that may help:

Double check the wiring connections: It's important to make sure that all the wiring connections between the display and the Pico are correct. Make sure to consult the datasheets and schematics of both the display and the Pico to ensure that you're connecting the correct pins.

Check the power supply: Make sure that the display is receiving adequate power. E-paper displays require a high voltage to operate, and it's possible that the power supply you're using isn't providing enough voltage.

Try a different library: If you're having trouble with the library you're currently using, try a different one. There are several libraries available for e-paper displays that work with the Raspberry Pi Pico, so try a few to see which one works best for your setup.

Verify the code: Make sure that the code you're using is correct and that it's compatible with your display and Pico. Double-check the code and verify that it matches the wiring connections.

Try a different display: If you're still having trouble, it's possible that the display itself is faulty. Try using a different display to see if that works with your Pico.

I hope these suggestions help you get your e-paper display working with your Raspberry Pi Pico. Good luck!

I then installed MicroPython using my old documentation as a tutorial. I had already downloaded a ZIP file of this entire repo so I copied the waveshare_epd folder into the RP2040 disk drive that appears when I plug it in while holding the bootloader button.

I installed MicroPython using my old documentation then followed ChatGPT's advice for writing a program in MicroPython using the Waveshare E-Paper library. I right-clicked a file and clicked Upload to / to move it into the Raspberry Pi Pico's harddrive.

Upload to Slash

I encountered many errors using ChatGPT and spent several hours going down a rabbit hole telling it everything that wasn't working. It told me that actually this wouldn't work with MircoPython and gave me the files and a workflow for how to install CircuitPython on a Raspberry Pi Pico. I held the bootloader button while plugging it in then dragged a UF2 file into the drive, causing the board to restart. Here is the full text of my conversation with ChatGPT, or just skip it and move on. The formatting may be slightly strange. In essence, there were so many hallucinations, and it was extremely unhelpful. I learned that I can be much more efficient if I just try to do it myself.

Different Approach

I give up - too much hallucination.

Luckily, I found this library that specifically deals with e-paper in MicroPython, but it doesn't go up to the 9.7" display, so for now I'll just focus on the 7.5" display.

Board not showing up - Press Bootsel, reset button, releaes Bootsel, release reset button.

When I selected < Try to detect port automatically > finally worked. Turns out it wasn't the 3V output plugged into the e-paper display.

from micropython import const
from time import sleep_ms
import ustruct

# Display resolution
EPD_WIDTH  = const(640)
EPD_HEIGHT = const(384)

# Display commands
PANEL_SETTING                  = const(0x00)
POWER_SETTING                  = const(0x01)
POWER_OFF                      = const(0x02)
#POWER_OFF_SEQUENCE_SETTING     = const(0x03)
POWER_ON                       = const(0x04)
#POWER_ON_MEASURE               = const(0x05)
BOOSTER_SOFT_START             = const(0x06)
DEEP_SLEEP                     = const(0x07)
DATA_START_TRANSMISSION_1      = const(0x10)
#DATA_STOP                      = const(0x11)
DISPLAY_REFRESH                = const(0x12)
#IMAGE_PROCESS                  = const(0x13)
#LUT_FOR_VCOM                   = const(0x20)
#LUT_BLUE                       = const(0x21)
#LUT_WHITE                      = const(0x22)
#LUT_GRAY_1                     = const(0x23)
#LUT_GRAY_2                     = const(0x24)
#LUT_RED_0                      = const(0x25)
#LUT_RED_1                      = const(0x26)
#LUT_RED_2                      = const(0x27)
#LUT_RED_3                      = const(0x28)
#LUT_XON                        = const(0x29)
PLL_CONTROL                    = const(0x30)
#TEMPERATURE_SENSOR_COMMAND     = const(0x40)
TEMPERATURE_CALIBRATION        = const(0x41)
#TEMPERATURE_SENSOR_WRITE       = const(0x42)
#TEMPERATURE_SENSOR_READ        = const(0x43)
VCOM_AND_DATA_INTERVAL_SETTING = const(0x50)
#LOW_POWER_DETECTION            = const(0x51)
TCON_SETTING                   = const(0x60)
TCON_RESOLUTION                = const(0x61)
#SPI_FLASH_CONTROL              = const(0x65)
#REVISION                       = const(0x70)
#GET_STATUS                     = const(0x71)
#AUTO_MEASUREMENT_VCOM          = const(0x80)
#READ_VCOM_VALUE                = const(0x81)
VCM_DC_SETTING                 = const(0x82)
FLASH_MODE                     = const(0xE5)

BUSY = const(0)  # 0=busy, 1=idle

class EPD:
    def __init__(self, spi, cs, dc, rst, busy):
        self.spi = spi
        self.cs = cs
        self.dc = dc
        self.rst = rst
        self.busy = busy
        self.cs.init(self.cs.OUT, value=1)
        self.dc.init(self.dc.OUT, value=0)
        self.rst.init(self.rst.OUT, value=0)
        self.busy.init(self.busy.IN)
        self.width = EPD_WIDTH
        self.height = EPD_HEIGHT

    def _command(self, command, data=None):
        self.dc(0)
        self.cs(0)
        self.spi.write(bytearray([command]))
        self.cs(1)
        if data is not None:
            self._data(data)

    def _data(self, data):
        self.dc(1)
        self.cs(0)
        self.spi.write(data)
        self.cs(1)

    def init(self):
        self.reset()
        self._command(POWER_SETTING, b'\x37\x00')
        self._command(PANEL_SETTING, b'\xCF\x08')
        self._command(BOOSTER_SOFT_START, b'\xC7\xCC\x28')
        self._command(POWER_ON)
        self.wait_until_idle()
        self._command(PLL_CONTROL, b'\x3C')
        self._command(TEMPERATURE_CALIBRATION, b'\x00')
        self._command(VCOM_AND_DATA_INTERVAL_SETTING, b'\x77')
        self._command(TCON_SETTING, b'\x22')
        self._command(TCON_RESOLUTION, ustruct.pack(">HH", EPD_WIDTH, EPD_HEIGHT))
        self._command(VCM_DC_SETTING, b'\x1E') # decide by LUT file
        self._command(FLASH_MODE, b'\x03')

    def wait_until_idle(self):
        while self.busy.value() == BUSY:
            sleep_ms(100)

    def reset(self):
        self.rst(0)
        sleep_ms(200)
        self.rst(1)
        sleep_ms(200)

    # draw the current frame memory
    def display_frame(self, frame_buffer):
        self._command(DATA_START_TRANSMISSION_1)
        for i in range(0, self.width * self.height // 8):
            temp1 = frame_buffer[i]
            j = 0
            while (j < 8):
                if (temp1 & 0x80):
                    temp2 = 0x03
                else:
                    temp2 = 0x00
                temp2 = (temp2 << 4) & 0xFF
                temp1 = (temp1 << 1) & 0xFF
                j += 1
                if (temp1 & 0x80):
                    temp2 |= 0x03
                else:
                    temp2 |= 0x00
                temp1 = (temp1 << 1) & 0xFF
                self._data(bytearray([temp2]))
                j += 1
        self._command(DISPLAY_REFRESH)
        sleep_ms(100)
        self.wait_until_idle()

    # to wake call reset() or init()
    def sleep(self):
        self._command(POWER_OFF)
        self.wait_until_idle()
        self._command(DEEP_SLEEP, b'\xA5')

import framebuf

epd = EPD(0, 17, 20, 21, 22)
#self, spi, cs, dc, rst, busy
epd.init()
buffer = bytearray(epd.width * epd.height // 8)
fb = framebuf.FrameBuffer(buffer, epd.width, epd.height, framebuf.MONO_HLSB)
fb.fill(0)
epd.display_frame(buffer)

fb.text("Hello, World!", 0, 0, 1)
epd.display_frame(buffer)

Didn't work because numbers in EPD constructor needed to be Pin and SPI objects.

from micropython import const
from time import sleep_ms
import ustruct

# Display resolution
EPD_WIDTH  = const(640)
EPD_HEIGHT = const(384)

# Display commands
PANEL_SETTING                  = const(0x00)
POWER_SETTING                  = const(0x01)
POWER_OFF                      = const(0x02)
#POWER_OFF_SEQUENCE_SETTING     = const(0x03)
POWER_ON                       = const(0x04)
#POWER_ON_MEASURE               = const(0x05)
BOOSTER_SOFT_START             = const(0x06)
DEEP_SLEEP                     = const(0x07)
DATA_START_TRANSMISSION_1      = const(0x10)
#DATA_STOP                      = const(0x11)
DISPLAY_REFRESH                = const(0x12)
#IMAGE_PROCESS                  = const(0x13)
#LUT_FOR_VCOM                   = const(0x20)
#LUT_BLUE                       = const(0x21)
#LUT_WHITE                      = const(0x22)
#LUT_GRAY_1                     = const(0x23)
#LUT_GRAY_2                     = const(0x24)
#LUT_RED_0                      = const(0x25)
#LUT_RED_1                      = const(0x26)
#LUT_RED_2                      = const(0x27)
#LUT_RED_3                      = const(0x28)
#LUT_XON                        = const(0x29)
PLL_CONTROL                    = const(0x30)
#TEMPERATURE_SENSOR_COMMAND     = const(0x40)
TEMPERATURE_CALIBRATION        = const(0x41)
#TEMPERATURE_SENSOR_WRITE       = const(0x42)
#TEMPERATURE_SENSOR_READ        = const(0x43)
VCOM_AND_DATA_INTERVAL_SETTING = const(0x50)
#LOW_POWER_DETECTION            = const(0x51)
TCON_SETTING                   = const(0x60)
TCON_RESOLUTION                = const(0x61)
#SPI_FLASH_CONTROL              = const(0x65)
#REVISION                       = const(0x70)
#GET_STATUS                     = const(0x71)
#AUTO_MEASUREMENT_VCOM          = const(0x80)
#READ_VCOM_VALUE                = const(0x81)
VCM_DC_SETTING                 = const(0x82)
FLASH_MODE                     = const(0xE5)

BUSY = const(0)  # 0=busy, 1=idle

class EPD:
    def __init__(self, spi, cs, dc, rst, busy):
        self.spi = spi
        self.cs = cs
        self.dc = dc
        self.rst = rst
        self.busy = busy
        self.cs.init(self.cs.OUT, value=1)
        self.dc.init(self.dc.OUT, value=0)
        self.rst.init(self.rst.OUT, value=0)
        self.busy.init(self.busy.IN)
        self.width = EPD_WIDTH
        self.height = EPD_HEIGHT

    def _command(self, command, data=None):
        self.dc(0)
        self.cs(0)
        self.spi.write(bytearray([command]))
        self.cs(1)
        if data is not None:
            self._data(data)

    def _data(self, data):
        self.dc(1)
        self.cs(0)
        self.spi.write(data)
        self.cs(1)

    def init(self):
        self.reset()
        self._command(POWER_SETTING, b'\x37\x00')
        self._command(PANEL_SETTING, b'\xCF\x08')
        self._command(BOOSTER_SOFT_START, b'\xC7\xCC\x28')
        self._command(POWER_ON)
        self.wait_until_idle()
        self._command(PLL_CONTROL, b'\x3C')
        self._command(TEMPERATURE_CALIBRATION, b'\x00')
        self._command(VCOM_AND_DATA_INTERVAL_SETTING, b'\x77')
        self._command(TCON_SETTING, b'\x22')
        self._command(TCON_RESOLUTION, ustruct.pack(">HH", EPD_WIDTH, EPD_HEIGHT))
        self._command(VCM_DC_SETTING, b'\x1E') # decide by LUT file
        self._command(FLASH_MODE, b'\x03')

    def wait_until_idle(self):
        while self.busy.value() == BUSY:
            sleep_ms(100)

    def reset(self):
        self.rst(0)
        sleep_ms(200)
        self.rst(1)
        sleep_ms(200)

    # draw the current frame memory
    def display_frame(self, frame_buffer):
        self._command(DATA_START_TRANSMISSION_1)
        for i in range(0, self.width * self.height // 8):
            temp1 = frame_buffer[i]
            j = 0
            while (j < 8):
                if (temp1 & 0x80):
                    temp2 = 0x03
                else:
                    temp2 = 0x00
                temp2 = (temp2 << 4) & 0xFF
                temp1 = (temp1 << 1) & 0xFF
                j += 1
                if (temp1 & 0x80):
                    temp2 |= 0x03
                else:
                    temp2 |= 0x00
                temp1 = (temp1 << 1) & 0xFF
                self._data(bytearray([temp2]))
                j += 1
        self._command(DISPLAY_REFRESH)
        sleep_ms(100)
        self.wait_until_idle()

    # to wake call reset() or init()
    def sleep(self):
        self._command(POWER_OFF)
        self.wait_until_idle()
        self._command(DEEP_SLEEP, b'\xA5')

from machine import Pin, SPI
import framebuf

spi = SPI(0, sck=Pin(18), mosi=Pin(19), miso=Pin(16), baudrate=2000000)
cs = Pin(17, Pin.OUT)
dc = Pin(20, Pin.OUT)
rst = Pin(21, Pin.OUT)
busy = Pin(22, Pin.IN)

epd = EPD(spi, cs, dc, rst, busy)
#self, spi, cs, dc, rst, busy
epd.init()
buffer = bytearray(epd.width * epd.height // 8)
fb = framebuf.FrameBuffer(buffer, epd.width, epd.height, framebuf.MONO_HLSB)
fb.fill(0)
epd.display_frame(buffer)

fb.text("Hello, World!", 0, 0, 1)
epd.display_frame(buffer)

This didn't work:

from micropython import const
from time import sleep_ms
import ustruct

# Display resolution
EPD_WIDTH  = const(640)
EPD_HEIGHT = const(384)

# Display commands
PANEL_SETTING                  = const(0x00)
POWER_SETTING                  = const(0x01)
POWER_OFF                      = const(0x02)
#POWER_OFF_SEQUENCE_SETTING     = const(0x03)
POWER_ON                       = const(0x04)
#POWER_ON_MEASURE               = const(0x05)
BOOSTER_SOFT_START             = const(0x06)
DEEP_SLEEP                     = const(0x07)
DATA_START_TRANSMISSION_1      = const(0x10)
#DATA_STOP                      = const(0x11)
DISPLAY_REFRESH                = const(0x12)
#IMAGE_PROCESS                  = const(0x13)
#LUT_FOR_VCOM                   = const(0x20)
#LUT_BLUE                       = const(0x21)
#LUT_WHITE                      = const(0x22)
#LUT_GRAY_1                     = const(0x23)
#LUT_GRAY_2                     = const(0x24)
#LUT_RED_0                      = const(0x25)
#LUT_RED_1                      = const(0x26)
#LUT_RED_2                      = const(0x27)
#LUT_RED_3                      = const(0x28)
#LUT_XON                        = const(0x29)
PLL_CONTROL                    = const(0x30)
#TEMPERATURE_SENSOR_COMMAND     = const(0x40)
TEMPERATURE_CALIBRATION        = const(0x41)
#TEMPERATURE_SENSOR_WRITE       = const(0x42)
#TEMPERATURE_SENSOR_READ        = const(0x43)
VCOM_AND_DATA_INTERVAL_SETTING = const(0x50)
#LOW_POWER_DETECTION            = const(0x51)
TCON_SETTING                   = const(0x60)
TCON_RESOLUTION                = const(0x61)
#SPI_FLASH_CONTROL              = const(0x65)
#REVISION                       = const(0x70)
#GET_STATUS                     = const(0x71)
#AUTO_MEASUREMENT_VCOM          = const(0x80)
#READ_VCOM_VALUE                = const(0x81)
VCM_DC_SETTING                 = const(0x82)
FLASH_MODE                     = const(0xE5)

BUSY = const(0)  # 0=busy, 1=idle

class EPD:
    def __init__(self, spi, cs, dc, rst, busy):
        self.spi = spi
        self.cs = cs
        self.dc = dc
        self.rst = rst
        self.busy = busy
        self.cs.init(self.cs.OUT, value=1)
        self.dc.init(self.dc.OUT, value=0)
        self.rst.init(self.rst.OUT, value=0)
        self.busy.init(self.busy.IN)
        self.width = EPD_WIDTH
        self.height = EPD_HEIGHT

    def _command(self, command, data=None):
        self.dc(0)
        self.cs(0)
        self.spi.write(bytearray([command]))
        self.cs(1)
        if data is not None:
            self._data(data)

    def _data(self, data):
        self.dc(1)
        self.cs(0)
        self.spi.write(data)
        self.cs(1)

    def init(self):
        print("started restart")
        self.reset()
        print("finished restarting")
        self._command(POWER_SETTING, b'\x37\x00')
        self._command(PANEL_SETTING, b'\xCF\x08')
        self._command(BOOSTER_SOFT_START, b'\xC7\xCC\x28')
        self._command(POWER_ON)
        print("waiting until idle")
        self.wait_until_idle()
        self._command(PLL_CONTROL, b'\x3C')
        self._command(TEMPERATURE_CALIBRATION, b'\x00')
        self._command(VCOM_AND_DATA_INTERVAL_SETTING, b'\x77')
        self._command(TCON_SETTING, b'\x22')
        self._command(TCON_RESOLUTION, ustruct.pack(">HH", EPD_WIDTH, EPD_HEIGHT))
        self._command(VCM_DC_SETTING, b'\x1E') # decide by LUT file
        self._command(FLASH_MODE, b'\x03')

    def wait_until_idle(self):
        while self.busy.value() == BUSY:
            sleep_ms(100)

    def reset(self):
        self.rst(0)
        sleep_ms(200)
        self.rst(1)
        sleep_ms(200)

    # draw the current frame memory
    def display_frame(self, frame_buffer):
        self._command(DATA_START_TRANSMISSION_1)
        for i in range(0, self.width * self.height // 8):
            temp1 = frame_buffer[i]
            j = 0
            while (j < 8):
                if (temp1 & 0x80):
                    temp2 = 0x03
                else:
                    temp2 = 0x00
                temp2 = (temp2 << 4) & 0xFF
                temp1 = (temp1 << 1) & 0xFF
                j += 1
                if (temp1 & 0x80):
                    temp2 |= 0x03
                else:
                    temp2 |= 0x00
                temp1 = (temp1 << 1) & 0xFF
                self._data(bytearray([temp2]))
                j += 1
        self._command(DISPLAY_REFRESH)
        sleep_ms(100)
        self.wait_until_idle()

    # to wake call reset() or init()
    def sleep(self):
        self._command(POWER_OFF)
        self.wait_until_idle()
        self._command(DEEP_SLEEP, b'\xA5')

from machine import Pin, SPI
import framebuf

print("creating objs")
spi = SPI(0, sck=Pin(18), mosi=Pin(19), miso=Pin(16), baudrate=2000000)
cs = Pin(17, Pin.OUT)
dc = Pin(20, Pin.OUT)
rst = Pin(21, Pin.OUT)
busy = Pin(22, Pin.IN)

print("initializing")
epd = EPD(spi, cs, dc, rst, busy)
#self, spi, cs, dc, rst, busy
print("epd instance created")
epd.init()
print("initialized")
buffer = bytearray(epd.width * epd.height // 8)
fb = framebuf.FrameBuffer(buffer, epd.width, epd.height, framebuf.MONO_HLSB)
fb.fill(0)
epd.display_frame(buffer)

print("ready to hello, world!")
fb.text("Hello, World!", 0, 0, 1)
epd.display_frame(buffer)

I thought I maybe had the wrong type of board. I also tried the 4.2" e-paper display using the example code and fixing the wiring, and nothing happened. Partway through I noticed that the screen had become red and there were some gray specks, but I couldn't get anything to be controlled. I realized that I had all of the ribbon cables upside down, but they still didn't work when I corrected them. I even tried a different Pico and asked ChatGPT about the wiring, but it was all failing.

Continue reading here.