Skip to content

Instantly share code, notes, and snippets.

@stecman
Last active August 4, 2022 01:41
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stecman/847dd15e80ba292c29da28ba62e972fe to your computer and use it in GitHub Desktop.
Save stecman/847dd15e80ba292c29da28ba62e972fe to your computer and use it in GitHub Desktop.
Good Display epaper/eink screen testing code - image data from computer over USART
#include "GDEPaper.h"
// AVR
extern "C" {
#include <util/delay.h>
}
#define DDR_SPI DDRB
#define DD_MOSI PB2
#define DD_SCK PB1
#define DDR_BASE DDRB
#define PORT_BASE PORTB
#define PT_CS PB0
#define PT_DC PB6
#define PT_RES PB4
#define PIN_BUSY PIND
#define PORT_BUSY PORTD
#define DDR_BUSY DDRD
#define PT_BUSY PD2
GoodDisplayEpaper::GoodDisplayEpaper(void)
{
init_pins();
// Default internal active state to false, though this is not guaranteed until reset
m_active = false;
}
void GoodDisplayEpaper::wake_panel(void)
{
// Put the device in a known state
// This also wakes the screen up if it was put in deep sleep
send_reset();
// Chip select enable (low)
PORT_BASE &= ~(1<<PT_CS);
_delay_us(1); // CS setup time is ~40ns on the display
// Soft start the panel's booster circuitry
// The argument values here are taken from the datasheet's reference program
// These appear to be balanced settings between speed vs power saving
const uint8_t btst_args[] = { 0x17, 0x17, 0x17 };
send_command(GD_BTST, btst_args, 3);
}
void GoodDisplayEpaper::sleep_panel(void)
{
// Set border to floating
send_command(GD_CDI, 0x0);
// Stop onboard voltage converters
send_command(GD_POWER_OFF);
// Put the display in deep sleep mode
// The argument here is a required check-code to confirm the command
send_command(GD_DSLP, 0xA5);
// Chip select disable (high)
PORT_BASE |= (1<<PT_CS);
}
void GoodDisplayEpaper::send_command(const uint8_t data)
{
wait_for_panel_ready();
// Data/Command line goes low for sending command bytes
PORT_BASE &= ~(1<<PT_DC);
send_spi(data);
}
void GoodDisplayEpaper::send_command(const uint8_t command, const uint8_t argument)
{
send_command(command);
send_data(argument);
}
void GoodDisplayEpaper::send_command(const uint8_t command, const uint8_t* arguments, uint8_t length)
{
send_command(command);
for (uint8_t i = 0; i < length; ++i) {
send_data(arguments[i]);
}
}
void GoodDisplayEpaper::send_data(const uint8_t* data, uint8_t length)
{
for (uint8_t i = 0; i < length; ++i) {
send_data(data[i]);
}
}
void GoodDisplayEpaper::send_data(const uint8_t data)
{
// wait_for_panel_ready();
// Data/Command line goes high for sending data
PORT_BASE |= (1<<PT_DC);
send_spi(data);
}
bool GoodDisplayEpaper::is_ready(void)
{
return (PIN_BUSY & _BV(PT_BUSY)) > 0;
}
// Internal
void GoodDisplayEpaper::init_pins(void)
{
// Control pins as outputs
DDR_BASE |= (1<<PT_CS) | (1<<PT_DC) | (1<<PT_RES);
// Drive reset high for normal state (reset occurs on RES low)
PORT_BASE |= (1<<PT_RES);
// Set SPI's SS pin high to prevent the SPI controller going into slave mode
// See http://www.avrfreaks.net/forum/avr-spi-problem
//PORT_BASE |= (1<<PT_DC);
// Busy pin as input
DDR_BUSY &= ~(1<<PT_BUSY);
// Start in command mode (DC low), with chip select disabled (CS high)
PORT_BASE &= ~(1<<PT_DC);
PORT_BASE |= (1<<PT_CS);
// Set SPI pins as output
DDR_SPI |= (1<<DD_MOSI) | (1<<DD_SCK);
// Enable SPI as master, set clock rate fck/2
SPCR = (1<<SPE) | (1<<MSTR);
SPSR = (1<<SPI2X);
}
void GoodDisplayEpaper::send_spi(uint8_t data)
{
// Send the data
SPDR = data;
// Wait for the transmission to complete
while (!(SPSR & (1<<SPIF)));
}
inline void GoodDisplayEpaper::wait_for_panel_ready(void)
{
DDRD |= (1<<PD3); // Debug port as output
// Wait for the panel to become ready
// TODO: Timeout here and retry, or abort and return to caller
while ( (PIN_BUSY & _BV(PT_BUSY)) == 0 ){
PORTD ^= (1<<PD3);
}
}
void GoodDisplayEpaper::send_reset(void)
{
// Reset panel by pulling reset line low
PORT_BASE &= ~(1<<PT_RES);
_delay_us(50);
PORT_BASE |= (1<<PT_RES);
_delay_us(50);
}
#pragma once
// AVR
extern "C" {
#include <avr/io.h>
}
#define GD_PSR 0x00 // Panel settings
#define GD_PWR 0x01 // Power setting
#define GD_BTST 0x06 // Booster soft start
#define GD_POWER_ON 0x04
#define GD_POWER_OFF 0x02
#define GD_DSLP 0x07 // Deep sleep
#define GD_CDI 0x50 // VCOM and data interval setting
#define GD_PLL 0x30 // PLL control (panel refresh rate?)
#define GD_TRES 0x61 // Resolution settings
#define GD_DTM1 0x10 // Start data transmission (old frame for BW mode, or black in BWR mode)
#define GD_DTM2 0x13 // Start data transmission (new frame for BW mode, or red in BWR mode)
#define GD_DSTOP 0x11 // End data transmission
#define GD_REFRESH 0x12 // Display refresh (DRF in datasheet)
/**
* Base class for Good Display E-paper products
*/
class GoodDisplayEpaper {
public:
GoodDisplayEpaper(void);
/// Ensure the display is in the required modes for operation
void wake_panel(void);
/// Power down the panel
void sleep_panel(void);
// Basic communication methods
void send_command(const uint8_t data);
void send_data(const uint8_t data);
// Helper methods for communication
void send_command(const uint8_t command, const uint8_t argument);
void send_command(const uint8_t command, const uint8_t* arguments, uint8_t length);
void send_data(const uint8_t* data, uint8_t length);
bool is_ready(void);
protected:
/// Ensure pins are setup correctly for operation
void init_pins(void);
/// Send a single byte over SPI
void send_spi(const uint8_t data);
/// Hardware reset
void send_reset(void);
/// Wait for the panel's busy line to go high ( = ready to accept input)
inline void wait_for_panel_ready(void);
/// Border black/white fill
bool m_active;
};
#include "GDEW042.h"
// Border fill masks
#define CDI_BORDER_BLACK 0b10000000
#define CDI_BORDER_WHITE 0b01000000
const uint8_t lut_vcom0[] =
{
0x00, 0x17, 0x00, 0x00, 0x00, 0x02,
0x00, 0x17, 0x17, 0x00, 0x00, 0x02,
0x00, 0x0A, 0x01, 0x00, 0x00, 0x01,
0x00, 0x0E, 0x0E, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
};
const uint8_t lut_ww[] = {
0x40, 0x17, 0x00, 0x00, 0x00, 0x02,
0x90, 0x17, 0x17, 0x00, 0x00, 0x02,
0x40, 0x0A, 0x01, 0x00, 0x00, 0x01,
0xA0, 0x0E, 0x0E, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
const uint8_t lut_bw[] = {
0x40, 0x17, 0x00, 0x00, 0x00, 0x02,
0x90, 0x17, 0x17, 0x00, 0x00, 0x02,
0x40, 0x0A, 0x01, 0x00, 0x00, 0x01,
0xA0, 0x0E, 0x0E, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
const uint8_t lut_wb[] = {
0x80, 0x17, 0x00, 0x00, 0x00, 0x02,
0x90, 0x17, 0x17, 0x00, 0x00, 0x02,
0x80, 0x0A, 0x01, 0x00, 0x00, 0x01,
0x50, 0x0E, 0x0E, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
const uint8_t lut_bb[] = {
0x80, 0x17, 0x00, 0x00, 0x00, 0x02,
0x90, 0x17, 0x17, 0x00, 0x00, 0x02,
0x80, 0x0A, 0x01, 0x00, 0x00, 0x01,
0x50, 0x0E, 0x0E, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
void GDEW042::wake_panel()
{
GoodDisplayEpaper::wake_panel();
// Configure controller to use it's onboard voltage conversion
const uint8_t pwr_args[] = {
0x03, // Use internal VDG and VDS DC/DC converters
0x00, // -16V/16V VGL/VGH voltage
0x2b, // VDH B/W pixel power selection 11V
0x2b, // VDL B/W pixel power selection -11V
0xff, // red pixel power selection (irrelevant but required)
};
send_command(GD_PWR, pwr_args, 5);
// Power on the panel
send_command(GD_POWER_ON);
// Configure panel with default resolution, B/W pixels, LUT from register
send_command(GD_PSR, (0b00111111));
// Set the refresh framerate to 200Hz
send_command(GD_PLL, 0b00111001);
// Set gate/source non-overlap period
send_command(0x60, 0b00000000);
// Set panel resolution
// This is marked in the datasheet as an override, but it appears to be required
// Arugments are two bytes each for HRES and VRES
const uint8_t tres_args[] = { 0x01, 0x90, 0x01, 0x2c };
send_command(GD_TRES, tres_args, 4);
// VCOM DC settings (VCM_DC)
send_command(0x82, 0b00010010);
// VCOM and data interval setting + border fill
send_command(GD_CDI, 0b00001111 | CDI_BORDER_WHITE);
// Send custom look-up tables
send_command(0x20, lut_vcom0, sizeof(lut_vcom0));
send_command(0x21, lut_ww, sizeof(lut_ww));
send_command(0x22, lut_bw, sizeof(lut_bw));
send_command(0x23, lut_wb, sizeof(lut_wb));
send_command(0x24, lut_bb, sizeof(lut_bb));
}
#pragma once
#include "GDEPaper.h"
class GDEW0213 : public GoodDisplayEpaper {
public:
void wake_panel(void);
};
// C
#include <stdlib.h>
// AVR
extern "C" {
#include <avr/io.h>
#include <avr/wdt.h>
#include <util/delay.h>
}
#include "GDEW042.h"
#define BAUD 9600
#define MYUBRR F_CPU/16/BAUD-1
void init_usart(void)
{
cli();
// Set baud rate to 76.8K (with 16MHz clock)
// This is hard-coded for now as I was having issues with the calculated value
UBRR0H = (uint8_t)(12 >> 8);
UBRR0L = (uint8_t)(12);
// Enable receiver and transmitter
UCSR0B = _BV(RXEN0) | _BV(TXEN0);
// Set frame format: 8 data, 1 stop bit
UCSR0C = (3<<UCSZ00);
UCSR0C &= ~(1<<USBS0);
sei();
}
void usart_transmit(uint8_t data)
{
// Wait for empty transmit buffer
while ( !( UCSR0A & (1<<UDRE0)) );
// Put data into buffer, sends the data
UDR0 = data;
}
uint8_t usart_receive(void)
{
// Wait for data to be received
while ( !(UCSR0A & (1<<RXC0)) );
// Get and return received data from buffer
return UDR0;
}
int main(void)
{
// Recover from any watchdog timer reset mishaps
wdt_reset();
wdt_disable();
// select minimal prescaler (max system speed)
CLKPR = 0x80;
CLKPR = 0x00;
init_usart();
GDEW042 display;
uint8_t datin;
for (;;) {
display.wake_panel();
display.send_command(GD_DTM1);
for (uint16_t i = 0; i < 15000; ++i) {
display.send_data( usart_receive() );
// usart_transmit( datin );
}
display.send_command(GD_DSTOP, 0x00); // Data stop
display.send_command(GD_DTM2);
for (uint16_t i = 0; i < 15000; ++i) {
display.send_data( usart_receive() );
}
display.send_command(GD_DSTOP, 0x00); // Data stop
display.send_command(GD_REFRESH); // Refresh display
display.sleep_panel();
}
return 0;
}
DEVICE = atmega64a
DEVICEDUDE = m64 # device passed to avrdude (this differs sometimes from the avr-gcc device name)
CLOCK = 12000000
PROGRAMMER = -c dragon_jtag
FUSES = -U hfuse:w:0xd9:m -U lfuse:w:0x24:m
SOURCES = $(shell find -name '*.cpp' -or -name '*.S')
OBJECTS = $(SOURCES:.cpp=.o)
AVRDUDE = avrdude $(PROGRAMMER) -p $(DEVICEDUDE)
COMPILE = avr-g++ -Wall -Os -DF_CPU=$(CLOCK) -mmcu=$(DEVICE)
COMPILE += -I -I. -I./lib/
COMPILE += -funsigned-char -funsigned-bitfields -fpack-struct -fshort-enums
COMPILE += -ffunction-sections -fdata-sections -Wl,--gc-sections
COMPILE += -Wl,--relax -mcall-prologues
COMPILE += -fno-exceptions -std=c++14
C_OPTIONS = -std=gnu99 -Wstrict-prototypes
# symbolic targets:
all: $(SOURCES) main.hex
.cpp.o:
$(COMPILE) -c $< -o $@
.S.o:
$(COMPILE) -x assembler-with-cpp -c $< -o $@
# "-x assembler-with-cpp" should not be necessary since this is the default
# file type for the .S (with capital S) extension. However, upper case
# characters are not always preserved on Windows. To ensure WinAVR
# compatibility define the file type manually.
.cpp.s:
$(COMPILE) -S $< -o $@
flash: all
$(AVRDUDE) -U flash:w:main.hex:i
# if you use a bootloader, change the command below appropriately:
load: all
bootloadHID main.hex
clean:
find -name '*.d' -exec rm {} +
find -name '*.o' -exec rm {} +
rm -f main.hex main.elf
# file targets:
main.elf: $(OBJECTS)
$(COMPILE) -o main.elf $(OBJECTS)
main.hex: main.elf
rm -f main.hex
avr-objcopy -j .text -j .data -O ihex main.elf main.hex
avr-size --format=avr --mcu=$(DEVICE) main.elf
# If you have an EEPROM section, you must also create a hex file for the
# EEPROM and add it to the "flash" target.
# Targets for code debugging and analysis:
disasm: main.elf
avr-objdump -d main.elf
cpp:
$(COMPILE) -E main.c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment