From 9df39ab4ae513e791392824575d3d61a799dc7ab Mon Sep 17 00:00:00 2001 From: Corey Vixie Date: Sun, 29 Mar 2020 11:24:42 -0700 Subject: [PATCH] Basic src migration from private repo --- Cargo.toml | 10 + rustfmt.toml | 0 src/instructions.rs | 861 ++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 184 ++++++++++ src/panel.rs | 84 +++++ src/spi.rs | 63 ++++ 6 files changed, 1202 insertions(+) create mode 100644 Cargo.toml create mode 100644 rustfmt.toml create mode 100644 src/instructions.rs create mode 100644 src/main.rs create mode 100644 src/panel.rs create mode 100644 src/spi.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1f776b5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "st7701s" +version = "0.1.0" +authors = ["Corey Vixie "] +edition = "2018" + +[dependencies] +num = "0.2.0" +spidev = "0.4.0" +enum_primitive = "0.1.1" diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..e69de29 diff --git a/src/instructions.rs b/src/instructions.rs new file mode 100644 index 0000000..0f6bfad --- /dev/null +++ b/src/instructions.rs @@ -0,0 +1,861 @@ +use std::convert::TryInto; + +use crate::panel::Mode; + +/// This is a 3-wire SPI implementation. Reads and writes share the SDA pin and +/// are performed half-duplex +/// +/// Unless otherwise noted, any command pairs that set and unset a "mode" (eg. +/// DISPON/DISPOFF) will have no effect if the display is already in the mode +/// being requested. Therefore, these commands should be safe to use in a +/// "write-only, read-never" workflow. +/// +/// NOTE: Some commands take many separate parameter words, most of which have at +/// least 8 bits of variability. Because of this, they aren't enumerated and +/// the vector is passed directly through. + +/// The write mode of the interface means the micro controller writes +/// commands and data to the LCD driver. 3-lines serial data packet contains +/// a control bit D/CX and a transmission byte. In 4-lines serial interface, +/// data packet contains just transmission byte and control bit D/CX is +/// transferred by the D/CX pin. If D/CX is “low”, the transmission byte is +/// interpreted as a command byte. If D/CX is “high”, the transmission byte +/// is command register as parameter. +#[derive(Clone)] +pub struct Command { + pub address: u8, + pub parameters: Vec, +} + +impl Command { + fn new(address: u8) -> Command { + Command { + address: address, + parameters: Vec::new(), + } + } + + fn arg(mut self, arg: u8) -> Command { + self.parameters.push(arg); + self + } + + fn args(mut self, args: &[u8]) -> Command { + self.parameters.extend_from_slice(args); + self + } + + pub fn serialize_address(&self) -> [u8; 2] { + [(self.address as u8), 0x00] + } + + pub fn serialize_parameter(parameter: u8) -> [u8; 2] { + [parameter, 0x01] + } +} + +#[derive(Copy, Clone)] +pub enum GammaCurve { + /// Gamma Curve 1 (G=2.2) + One = 0x01, + /// Reserved + Two = 0x02, + /// Reserved + Three = 0x04, + /// Reserved + Four = 0x08, +} + +#[derive(Copy, Clone)] +pub enum TearingEffect { + /// V-blanking only + VBlank = 0x00, + /// V-blanking and H-blanking + VHBlank = 0x01, +} + +#[derive(Copy, Clone)] +pub enum DataEnable { + DE = 0x00, + HV = 0x80, +} + +#[derive(Copy, Clone)] +pub enum VsyncActive { + Low = 0x00, + High = 0x08, +} +#[derive(Copy, Clone)] +pub enum HsyncActive { + Low = 0x00, + High = 0x04, +} +#[derive(Copy, Clone)] +pub enum DataPolarity { + Rising = 0x00, + Falling = 0x02, +} +#[derive(Copy, Clone)] +pub enum EnablePolarity { + Low = 0x00, + High = 0x01, +} + +#[derive(Copy, Clone)] +pub enum PWMPolarity { + Low = 0x00, + High = 0x20, +} + +#[derive(Copy, Clone)] +pub enum LEDPolarity { + Low = 0x00, + High = 0x10, +} + +#[derive(Copy, Clone)] +pub enum PixelPinout { + Normal = 0x00, + Condensed = 0x08, +} +#[derive(Copy, Clone)] +pub enum EndPixelFormat { + SelfMSB = 0x00, + GreenMSB = 0x01, + SelfLSB = 0x02, + Zero = 0x04, + One = 0x05, +} + +#[derive(Copy, Clone)] +pub enum ScanDirection { + Normal = 0x00, + Reverse = 0x10, +} + +#[derive(Copy, Clone)] +pub enum ColorOrder { + /// RGB mode + Rgb = 0x00, + /// BGR mode + Bgr = 0x08, +} + +#[derive(Copy, Clone)] +pub enum BitsPerPixel { + /// 16 bits per pixel (RGB565) + Rgb565 = 0x50, + /// 18 bits per pixel (RGB666) + Rgb666 = 0x60, + /// 24 bits per pixel (RGB888) + Rgb888 = 0x70, +} + +#[derive(Copy, Clone)] +pub enum BrightnessControl { + /// Ignore display brightness value and soft-set it to 0x00 + Off = 0x00, + /// Use display brightness value normally + On = 0x20, +} + +#[derive(Copy, Clone)] +pub enum DisplayDimming { + /// Ignore display brightness value and soft-set it to 0x00 + Off = 0x00, + /// Use display brightness value normally + On = 0x08, +} + +#[derive(Copy, Clone)] +pub enum Backlight { + /// Disable backlight circuit. Control lines must be low. + Off = 0x00, + /// Enable backlight circuit. Normal behavior. + On = 0x04, +} + +#[derive(Copy, Clone)] +pub enum Enhancement { + /// Disable color enhancement + Off = 0x00, + /// Enable color enhancement + On = 0x80, +} + +#[derive(Copy, Clone)] +pub enum EnhancementMode { + Low = 0x00, + Medium = 0x10, + High = 0x30, +} + +#[derive(Copy, Clone)] +pub enum AdaptiveBrightness { + /// Off + Off = 0x00, + /// User Interface Mode + UserInterface = 0x01, + /// Still Picture Mode + StillPicture = 0x02, + /// Moving Image Mode + MovingImage = 0x03, +} + +#[derive(Copy, Clone)] +pub enum Inversion { + OneDot = 0x00, + TwoDot = 0x01, + Column = 0x07, +} + +#[derive(Copy, Clone)] +pub enum GammaOPBias { + Off = 0x00, + Min = 0x40, + Middle = 0x80, + Max = 0xC0, +} + +#[derive(Copy, Clone)] +pub enum SourceOPInput { + Off = 0x00, + Min = 0x04, + Middle = 0x08, + Max = 0x0C, +} + +#[derive(Copy, Clone)] +pub enum SourceOPOutput { + Off = 0x00, + Min = 0x01, + Middle = 0x02, + Max = 0x03, +} + +#[derive(Copy, Clone)] +pub enum VoltageAVDD { + Pos6_2 = 0x00, + Pos6_4 = 0x10, + Pos6_6 = 0x20, + Pos6_8 = 0x30, +} + +#[derive(Copy, Clone)] +pub enum VoltageAVCL { + Neg4_4 = 0x00, + Neg4_6 = 0x01, + Neg4_8 = 0x02, + Neg5_0 = 0x03, +} + +#[derive(Copy, Clone)] +pub enum SunlightReadable { + /// DEFAULT: Sunlight readable mode off + Off = 0x00, + /// Enable sunlight readable mode + On = 0x10, +} + +#[derive(PartialEq)] +pub enum Command2Selection { + Disabled = 0x00, + BK0 = 0x10, + BK1 = 0x11, +} + +#[derive(Copy, Clone)] +pub enum CommandsGeneral { + NOP = 0x00, // No-op + SWRESET = 0x01, // Software Reset + RDDID = 0x04, // Read Display ID + RDNUMED = 0x05, // Read Number of Errors on DSI + RDRED = 0x06, // Read the first pixel of Red Color + RDGREEN = 0x07, // Read the first pixel of Green Color + RDBLUE = 0x08, // Read the first pixel of Blue Color + RDDPM = 0x0A, // Read Display Power Mode + RDDMADCTL = 0x0B, // Read Display MADCTL + RDDCOLMOD = 0x0C, // Read Display Pixel Format + RDDIM = 0x0D, // Read Display Image Mode + RDDSM = 0x0E, // Read Display Signal Mode + RDDSDR = 0x0F, // Read Display Self-Diagnostic Result + SLPIN = 0x10, // Sleep in + SLPOUT = 0x11, // Sleep Out + PTLON = 0x12, // Partial Display Mode On + NORON = 0x13, // Normal Display Mode On + INVOFF = 0x20, // Display Inversion Off + INVON = 0x21, // Display Inversion On + ALLPOFF = 0x22, // All Pixel Off + ALLPON = 0x23, // All Pixel ON + GAMSET = 0x26, // Gamma Set + DISPOFF = 0x28, // Display Off + DISPON = 0x29, // Display On + TEOFF = 0x34, // Tearing Effect Line OFF + TEON = 0x35, // Tearing Effect Line ON + MADCTL = 0x36, // Display data access control + IDMOFF = 0x38, // Idle Mode Off + IDMON = 0x39, // Idle Mode On + COLMOD = 0x3A, // Interface Pixel Format + GSL = 0x45, // Get Scan Line + WRDISBV = 0x51, // Write Display Brightness + RDDISBV = 0x52, // Read Display Brightness Value + WRCTRLD = 0x53, // Write CTRL Display + RDCTRLD = 0x54, // Read CTRL Value Display + WRCACE = 0x55, // Write Content Adaptive Brightness Control and Color Enhancement + RDCABC = 0x56, // Read Content Adaptive Brightness Control + WRCABCMB = 0x5E, // Write CABC Minimum Brightness + RDCABCMB = 0x5F, // Read CABC Minimum Brightness + RDABCSDR = 0x68, // Read Automatic Brightness Control Self-Diagnostic Result + RDBWLB = 0x70, // Read Black/White Low Bits + RDBkx = 0x71, // Read Bkx + RDBky = 0x72, // Read Bky + RDWx = 0x73, // Read Wx + RDWy = 0x74, // Read Wy + RDRGLB = 0x75, // Read Red/Green Low Bits + RDRx = 0x76, // Read Rx + RDRy = 0x77, // Read Ry + RDGx = 0x78, // Read Gx + RDGy = 0x79, // Read Gy + RDBALB = 0x7A, // Read Blue/A Color Low Bits + RDBx = 0x7B, // Read Bx + CND2BKxSEL = 0xFF, // Set Command2 mode for BK Register +} + +#[derive(Copy, Clone)] +pub enum BK0Command2 { + PVGAMCTRL = 0xB0, // Positive Voltage Gamma Control + NVGAMCTRL = 0xB1, // Negative Voltage Gamma Control + DGMEN = 0xB8, // Digital Gamma Enable + DGMLUTR = 0xB9, // Digital Gamma Look-up Table for Red + DGMLUTB = 0xBA, // Digital Gamma Look-up Table for Blue + PWMCLKSEL = 0xBC, // PWM CLK select + LNESET = 0xC0, // Display Line Setting + PORCTRL = 0xC1, // Porch Control + INVSET = 0xC2, // Inversion selection & Frame Rate Control + RGBCTRL = 0xC3, // RGB control + PARCTRL = 0xC5, // Partial Mode Control + SDIR = 0xC7, // X-direction Control + PDOSET = 0xC8, // Pseudo-Dot inversion diving setting + COLCTRL = 0xCD, // Color Control + SRECTRL = 0xE0, // Sunlight Readable Enhancement + NRCTRL = 0xE1, // Noise Reduce Control + SECTRL = 0xE2, // Sharpness Control + CCCTRL = 0xE3, // Color Calibration Control + SKCTRL = 0xE4, // Skin Tone Preservation CONTROL +} + +#[derive(Copy, Clone)] +pub enum BK1Command2 { + VRHS = 0xB0, // Vop Amplitude setting + VCOMS = 0xB1, // VCOM amplitude setting + VGHSS = 0xB2, // VGH Voltage setting + TESTCMD = 0xB3, // TEST Command Setting + VGLS = 0xB5, // VGL Voltage setting + PWCTRL1 = 0xB7, // Power Control 1 + PWCTRL2 = 0xB8, // Power Control 2 + PCLKS1 = 0xBA, // Power pumping clk selection 1 + PCLKS3 = 0xBC, // Power pumping clk selection 3 + SPD1 = 0xC1, // Source pre_drive timing set1 + SPD2 = 0xC2, // Source pre_drive timing set2 + MIPISET1 = 0xD0, // MIPI Setting 1 + MIPISET2 = 0xD1, // MIPI Setting 2 + MIPISET3 = 0xD2, // MIPI Setting 3 + MIPISET4 = 0xD3, // MIPI Setting 4 +} + +impl CommandsGeneral { + /// # NO OPERATION + /// + /// This command is "empty". It has no effect on the display, but it can be + /// used to terminate parameter write commands. + pub fn no_operation() -> Result { + Ok(Command::new(Self::NOP as u8)) + } + + /// # SOFTWARE RESET + /// + /// The display module performs a software reset. Registers are written with + /// the default "reset" values. + /// + /// - Frame buffer contents are unaffected by this command + /// - After a SWRESET command, sleep at least 5ms before the next command + /// - If the display is sleeping when a SWRESET is sent, the sleep + /// duration should be at least 120ms before sending the next command. + /// - SWRESET cannot be sent during SLPOUT + /// - (MIPI ONLY) Send a shutdown packet before SWRESET + pub fn software_reset() -> Result { + Ok(Command::new(Self::SWRESET as u8)) + } + /// # SLEEP IN + /// + /// This command causes the display module to enter a minimum power state. + /// The buck converter, display oscilator, and panel scanning are all shut + /// down. + /// + /// The control interface, display data, and registers remain active. + /// + /// The driver may send PCLK, HS, and CS information after SLPIN, and this + /// data will be valid for the next two frames if Normal Mode is active. + /// + /// Dimming will not work when changing from sleep out to sleep in. + /// + /// Normally, sleep state can be read with RDDST, but MISO must be connected. + pub fn sleep_mode_on() -> Result { + Ok(Command::new(Self::SLPIN as u8)) + } + + /// # SLEEP OUT + /// + /// This command turns off the minimum power state set by SLPIN. + /// + /// The driver may send PCLK, HS, and CS information before SLPOUT, and this + /// data will be valid for the two frames before the command if Normal Mode + /// is active. + pub fn sleep_mode_off() -> Result { + Ok(Command::new(Self::SLPOUT as u8)) + } + /// # PARTIAL MODE ON + /// + /// This command turns on Partial Mode. See PARTIAL AREA (30h) command. + pub fn partial_mode_on() -> Result { + Ok(Command::new(Self::PTLON as u8)) + } + /// # NORMAL MODE ON (DEFAULT) + /// + /// This command turns on Normal Mode and turns off Partial Mode. + pub fn normal_mode_on() -> Result { + Ok(Command::new(Self::NORON as u8)) + } + /// # DISPLAY INVERSION OFF (DEFAULT) + /// + /// This command restores normal pixel values. + pub fn invert_display_off() -> Result { + Ok(Command::new(Self::INVOFF as u8)) + } + /// # DISPLAY INVERSION ON + /// + /// This command inverts the display (white becomes black, red becomes blue). + pub fn invert_display_on() -> Result { + Ok(Command::new(Self::INVON as u8)) + } + /// # ALL PIXELS OFF (BLACK) + /// + /// This command sets all pixel values to black. + /// + /// ALLPOFF may be used in Sleep Mode, Normal Mode, or Partial Mode. + pub fn all_pixels_off() -> Result { + Ok(Command::new(Self::ALLPOFF as u8)) + } + /// # ALL PIXELS ON (WHITE) + /// + /// This command sets all pixel values to white. + /// + /// ALLPOFF may be used in Sleep Mode, Normal Mode, or Partial Mode. + pub fn all_pixels_on() -> Result { + Ok(Command::new(Self::ALLPON as u8)) + } + /// # GAMMA CURVE SELECT + /// + /// This command selects a predefined gamma curve from one of four values. + /// + /// WARNING: It's not clear from the Sitronix documentation what any values + /// are aside from 01. + /// + ///| D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | + ///| -- | -- | -- | -- | -- | GC[3:0] | + pub fn gamma_curve_select(GC: GammaCurve) -> Result { + Ok(Command::new(Self::GAMSET as u8).arg(GC as u8)) + } + /// # DISPLAY OFF (DEFAULT?) + /// + /// This command is used to enter Display Off Mode. In this mode, display + /// data is disabled and all pixels are blanked. + /// + /// NOTE: It's possible that this is the default value. + pub fn display_off() -> Result { + Ok(Command::new(Self::DISPOFF as u8)) + } + /// # DISPLAY ON + /// + /// WARNING: I have no idea how this behaves. The Sitronix docs monkey copied + /// and pasted the description for DISPOFF. At a guess, it should turn the + /// display back on. + pub fn display_on() -> Result { + Ok(Command::new(Self::DISPON as u8)) + } + /// # TEARING EFFECT LINE OFF + /// + /// This command is used to turn off the display module's Tearing Effect + /// output signal (vsync?) on the TE signal line (active low). + pub fn tearing_effect_off() -> Result { + Ok(Command::new(Self::TEOFF as u8)) + } + /// # TEARING EFFECT LINE ON + /// + /// This command is used to turn on the display module's Tearing Effect + /// output signal line. + /// + ///| D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | + ///| -- | -- | -- | -- | -- | -- | -- | TE | + pub fn tearing_effect_on(TE: TearingEffect) -> Result { + Ok(Command::new(Self::TEON as u8).arg(TE as u8)) + } + /// # DISPLAY DATA ACCESS CONTROL + /// * [ML] - Scan direction + /// * [] + ///| D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | + ///| -- | -- | -- | ML | CO | -- | -- | -- | + pub fn display_data_control( + ML: ScanDirection, + CO: ColorOrder, + ) -> Result { + Ok(Command::new(Self::MADCTL as u8).arg(ML as u8 | CO as u8)) + } + /// # IDLE MODE OFF + /// + /// Turns off Idle Mode. Display is capable of its full 16.7 million color + /// palette + pub fn idle_mode_off() -> Result { + Ok(Command::new(Self::IDMOFF as u8)) + } + /// # IDLE MODE ON + /// + /// Turns on Idle Mode. In idle mode the color palette is significantly + /// reduced. The MSB of each color will be rounded up or down, creating a + /// palette limited to 8 colors. + pub fn idle_mode_on() -> Result { + Ok(Command::new(Self::IDMON as u8)) + } + /// # SET INTERFACE PIXEL FORMAT + /// + /// Defines the format for RGB pixel data. + /// + ///| D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | + ///| -- | BPP[2:0] | -- | -- | -- | -- | + pub fn set_color_mode(BPP: BitsPerPixel) -> Result { + Ok(Command::new(Self::COLMOD as u8).arg(BPP as u8)) + } + /// # WRDISBV + /// + /// Change the display brightness to an 8-bit value. + /// + /// 0x00: Lowest brightness + /// 0xFF: Hightest brightness + /// + ///| D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | + ///| Display Brightness Value [7:0] | + pub fn set_display_brightness(DBV: u8) -> Result { + Ok(Command::new(Self::WRDISBV as u8).arg(DBV as u8)) + } + + /// # WRITE CTRL DISPLAY + /// + /// This command changes more general behavior of the brightness controls. + /// + /// [BCTRL] Brightness control on or off + /// [DD] Display dimming (only affects manual brightness settings) + /// [BL] Backlight control on or off + /// + ///| D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | + ///| -- | -- | BCTRL | -- | DD | BL | -- | -- | + pub fn configure_brightness( + BCTRL: BrightnessControl, + DD: DisplayDimming, + BL: Backlight, + ) -> Result { + Ok(Command::new(Self::WRCTRLD as u8).arg(BCTRL as u8 | DD as u8 | BL as u8)) + } + /// # WRITE CONTENT ADAPTIVE BRIGHTNESS CONTROL AND COLOR ENHANCEMENT + /// + /// Set parameters for content-based adaptive brightness control, set + /// different color enhancement modes. + /// + /// [CE] Color enhancement on or off: + /// [CEMD] Color enhancement mode + /// [CABC] Adaptive brightness control + /// + ///| D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | + ///| CE | -- | CEMD[1:0] | -- | -- | CABC[1:0] | + pub fn configure_color_enhancement( + CE: Enhancement, + CEMD: EnhancementMode, + CABC: AdaptiveBrightness, + ) -> Result { + Ok(Command::new(Self::WRCACE as u8).arg(CE as u8 | CEMD as u8 | CABC as u8)) + } + + /// + /// WRITE CABC MINIMUM BRIGHTNESS + /// + /// Sets the minimum brightness value to be used for CABC (see WRCACE). + /// + /// [MBV] Minimum Brightness Value + /// + ///| D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | + ///| Minimum Brightness Value [7:0] | + pub fn set_minimum_brightness(MBV: u8) -> Result { + Ok(Command::new(Self::WRCABCMB as u8).arg(MBV as u8)) + } + + pub fn read_display_pixel_format() -> Result { + Ok(Command::new(Self::RDDCOLMOD as u8)) + } + + pub fn read_self_diagnostics() -> Result { + Ok(Command::new(Self::RDDSDR as u8)) + } + + /// # SET COMMAND2 MODE + /// This is one of the most confusing attributes of the Sitronix chips. + /// BK0, BK1, and BK3 (maybe) all have "Command2" instructions that share a + /// common address space. To avoid collisions and to ensure you're sending + /// the command you think you're sending, we use a double-entry bookkeeping + /// approach, where set_command_2 will send the chip the updated Command2 + /// setting AND record it back to the local flag, which is required for + /// static type checking in all Command2 instructions locally. + /// + /// eg. for a BK1 Command2 instruction, "current" must be set to + /// Command2Selection::BK1. + pub fn set_command_2(set: Command2Selection) -> Result { + Ok(Command::new(Self::CND2BKxSEL as u8).args(&[0x77, 0x01, 0x00, 0x00, set as u8])) + } +} + +impl BK0Command2 { + pub fn validate(CMD2: &Command2Selection, build_command: F) -> Result + where + F: Fn() -> Command, + { + match CMD2 { + Command2Selection::BK0 => Ok(build_command()), + _ => Err("Cannot run command '{}': BK0 Command 2 mode not set"), + } + } + + /// # POSITIVE GAMMA CONTROL + /// See note above about parameters + pub fn positive_gamma_control( + CMD2: &Command2Selection, + parameters: &[u8], + ) -> Result { + Self::validate(CMD2, || { + Command::new(Self::PVGAMCTRL as u8).args(parameters) + }) + } + + /// # POSITIVE GAMMA CONTROL + /// See note above about parameters + pub fn negative_gamma_control( + CMD2: &Command2Selection, + parameters: &[u8], + ) -> Result { + Self::validate(CMD2, || { + Command::new(Self::NVGAMCTRL as u8).args(parameters) + }) + } + + /// # DISPLAY LINE SETTING + pub fn display_line_setting( + CMD2: &Command2Selection, + LDE_EN: u8, + Line: u8, + Line_delta: u8, + ) -> Result { + Self::validate(CMD2, || { + Command::new(Self::LNESET as u8).args(&[LDE_EN | Line, Line_delta]) + }) + } + + /// # PORCH CONTROL + pub fn porch_control(CMD2: &Command2Selection, mode: &Mode) -> Result { + let front_porch: u8 = (mode.vtotal - mode.vsync_end).try_into().unwrap(); + let back_porch: u8 = (mode.vsync_start - mode.vdisplay).try_into().unwrap(); + Self::validate(CMD2, || { + Command::new(Self::PORCTRL as u8).args(&[front_porch, back_porch]) + }) + } + + /// # INVERSION SELECT + /// * [LINV] - the type of inversion + /// * [RTNI] - minimum number of pclk in each line + pub fn inversion_select( + CMD2: &Command2Selection, + NLINV: Inversion, + RTNI: u8, + ) -> Result { + Self::validate(CMD2, || { + Command::new(Self::INVSET as u8).args(&[0x30 | NLINV as u8, RTNI]) + }) + } + + /// # RGB CONTROLDE/HV:RGB Mode selection + /// * [DEHV] + /// 0: RGB DE mode + /// 1: RGB HV mode + /// * [VSP]: Sets the signal polarity of the VSYNC pin. + /// 0: Low active + /// 1: High active + /// * [HSP]: Sets the signal polarity of the HSYNC pin. + /// 0: Low active + /// 1: High active + /// * [DP]: Sets the signal polarity of the DOTCLK pin. + /// 0: The data is input on the positive edge of DOTCLK + /// 1: The data is input on the negative edge of DOTCLK + /// * [EP]: Sets the signal polarity of the ENABLE pin. + /// 0: The data DB23-0 is written when ENABLE = “1". Disable data write operation when ENABLE = “0”. + /// 1: The data DB23-0 is written when ENABLE = “0”. Disable data write operation when ENABLE = “1”. + ///| D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | + ///| DEHV | -- | -- | -- | VSP | HSP | DP | EP | + ///| HBP | + ///| VBP | + pub fn rgb_control( + CMD2: &Command2Selection, + DEHV: DataEnable, + VSP: VsyncActive, + HSP: HsyncActive, + DP: DataPolarity, + EP: EnablePolarity, + mode: &Mode, + ) -> Result { + let HBP: u8 = (mode.htotal - mode.hsync_end).try_into().unwrap(); + let VBP: u8 = (mode.vsync_start - mode.vdisplay).try_into().unwrap(); + + Self::validate(CMD2, || { + Command::new(Self::RGBCTRL as u8).args(&[ + DEHV as u8 | VSP as u8 | HSP as u8 | DP as u8 | EP as u8, + HBP, + VBP, + ]) + }) + } + + /// # COLOR CONTROL + /// * [PWM]: LEDPWM polarity control. + /// 0: polarity normal. + /// 1: polarity reverse. + /// * [LED]: LED_ON polarity control. + /// 0: polarity normal. + /// 1: polarity reverse. + /// * [MDT]: RGB pixel format argument.(for 262K).See Table 17. + /// 0: pixel format argument normal. + /// 1: pixel collect to DB[17:0]. + /// * [EPF][2:0]: end of pixel format (for 65k & 262k mode) + /// 0: copy self MSB + /// 1: copy G MSB + /// 2: copy self LSB + /// 4: FIX 0 + /// 5: FIX 1 + ///| D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | + ///| -- | -- | PWM | LED | MDT | EPF | + pub fn color_control( + CMD2: &Command2Selection, + PWM: PWMPolarity, + LED: LEDPolarity, + MDT: PixelPinout, + EPF: EndPixelFormat, + ) -> Result { + Self::validate(CMD2, || { + Command::new(Self::COLCTRL as u8).arg(PWM as u8 | LED as u8 | MDT as u8 | EPF as u8) + }) + } + + /// # CONFIGURE SUNLIGHT READABLE ENHANCEMENT MODE + /// + /// Sets the minimum brightness value to be used for CABC (see WRCACE). + /// + /// [MBV] Minimum Brightness Value + /// + ///| D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | + ///| -- | -- | -- | -- | SRE | SRE_alpha[3:0] | + pub fn configure_sunlight_ehancement( + CMD2: &Command2Selection, + SRE: SunlightReadable, + mut SRE_alpha: u8, + ) -> Result { + if SRE_alpha > 0x0F { + SRE_alpha = 0x0F; + } + Self::validate(CMD2, || { + Command::new(Self::SECTRL as u8).arg(SRE as u8 | SRE_alpha) + }) + } +} + +impl BK1Command2 { + pub fn validate(CMD2: &Command2Selection, build_command: F) -> Result + where + F: Fn() -> Command, + { + match CMD2 { + Command2Selection::BK1 => Ok(build_command()), + _ => Err("Cannot run command '{}': BK0 Command 2 mode not set"), + } + } + + pub fn set_vop_amplitude(CMD2: &Command2Selection, VRHA: u8) -> Result { + Self::validate(CMD2, || Command::new(BK1Command2::VRHS as u8).arg(VRHA)) + } + + pub fn set_vcom_amplitude(CMD2: &Command2Selection, VCOM: u8) -> Result { + Self::validate(CMD2, || Command::new(BK1Command2::VCOMS as u8).arg(VCOM)) + } + + pub fn set_vgh_voltage(CMD2: &Command2Selection, VGH: u8) -> Result { + Self::validate(CMD2, || Command::new(BK1Command2::VGHSS as u8).arg(VGH)) + } + pub fn test_command_setting(CMD2: &Command2Selection) -> Result { + Self::validate(CMD2, || Command::new(BK1Command2::TESTCMD as u8).arg(0x80)) + } + + pub fn set_vgl_voltage(CMD2: &Command2Selection, VGLS: u8) -> Result { + Self::validate(CMD2, || { + Command::new(BK1Command2::VGLS as u8).arg(0x40 | VGLS) + }) + } + + pub fn power_control_one( + CMD2: &Command2Selection, + AP: GammaOPBias, + APIS: SourceOPInput, + APOS: SourceOPOutput, + ) -> Result { + Self::validate(CMD2, || { + Command::new(BK1Command2::PWCTRL1 as u8).arg(AP as u8 | APIS as u8 | APOS as u8) + }) + } + + pub fn power_control_two( + CMD2: &Command2Selection, + AVDD: VoltageAVDD, + AVCL: VoltageAVCL, + ) -> Result { + Self::validate(CMD2, || { + Command::new(BK1Command2::PWCTRL2 as u8).arg(AVDD as u8 | AVCL as u8) + }) + } + + /// # SET SOURCE PRE DRIVE TIMING CONTROL + /// T2D [3:0]: source pre_drive timing setting.(GND to VDD) + /// Adjust Range : 0 ~ 3 uS 1 step is 0.2uS + ///| D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | + ///| -- | 1 | 1 | 1 | T2D | + pub fn set_pre_drive_timing_one( + CMD2: &Command2Selection, + T2D: u8, + ) -> Result { + Self::validate(CMD2, || { + Command::new(BK1Command2::SPD1 as u8).arg(0x70 | T2D) + }) + } + + /// # SET SOURCE PRE DRIVE TIMING CONTROL + /// Same parameters as SPD1 + pub fn set_pre_drive_timing_two( + CMD2: &Command2Selection, + T2D: u8, + ) -> Result { + Self::validate(CMD2, || { + Command::new(BK1Command2::SPD2 as u8).arg(0x70 | T2D) + }) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..97ce9aa --- /dev/null +++ b/src/main.rs @@ -0,0 +1,184 @@ +extern crate enum_primitive; +extern crate num; +extern crate spidev; +use std::{thread, time}; + +mod instructions; +mod panel; +mod spi; + +use instructions::{BK0Command2, BK1Command2, Command, Command2Selection, CommandsGeneral}; +use panel::CVTRB; +use spi::ST7701S; + +/// ST7701S supports two kinds of RGB interface, DE mode (mode 1) and HV mode +/// (mode 2), and 16bit/18bit and 24 bit data format. When DE mode is selected +/// and the VSYNC, HSYNC, DOTCLK, DE, D23:0 pins can be used; when HV mode is +/// selected and the VSYNC, HSYNC, DOTCLK, D23:0 pins can be used. When using +/// RGB interface, only serial interface can be selected. +fn main() { + println!("Initializing SPI driver for ST7701S panel"); + + let mut CMD2: Command2Selection = Command2Selection::Disabled; + let mut display = ST7701S::new(String::from("/dev/spidev1.0")); + let mode = CVTRB; + + // SOFTWARE RESET + // 5ms delay + display.write_command(CommandsGeneral::software_reset()); + thread::sleep(time::Duration::from_millis(5)); + + // EXIT SLEEP MODE + // Variable delay (200ms is "safe") + display.write_command(CommandsGeneral::sleep_mode_off()); + thread::sleep(time::Duration::from_millis(200)); + + // ENTER BK0 COMMAND2 MODE + display.write_command(CommandsGeneral::set_command_2(Command2Selection::BK0)); + CMD2 = Command2Selection::BK0; + + display.write_command(BK0Command2::positive_gamma_control( + &CMD2, + &[ + 0x00, 0x0E, 0x15, 0x0F, 0x11, 0x08, 0x08, 0x08, 0x08, 0x23, 0x04, 0x13, 0x12, 0x2B, + 0x34, 0x1F, + ], + )); + display.write_command(BK0Command2::negative_gamma_control( + &CMD2, + &[ + 0x00, 0x0E, 0x95, 0x0F, 0x13, 0x07, 0x09, 0x08, 0x08, 0x22, 0x04, 0x10, 0x0E, 0x2C, + 0x34, 0x1F, + ], + )); + display.write_command(BK0Command2::display_line_setting(&CMD2, 0x80, 0x69, 0x02)); + display.write_command(BK0Command2::porch_control(&CMD2, &mode)); + display.write_command(BK0Command2::inversion_select( + &CMD2, + instructions::Inversion::Column, + 0xFF, + )); + display.write_command(BK0Command2::rgb_control( + &CMD2, + instructions::DataEnable::DE, + instructions::VsyncActive::Low, + instructions::HsyncActive::Low, + instructions::DataPolarity::Rising, + instructions::EnablePolarity::Low, + &mode, + )); + + // ENTER BK1 COMMAND2 MODE + display.write_command(CommandsGeneral::set_command_2(Command2Selection::BK1)); + CMD2 = Command2Selection::BK1; + + display.write_command(BK1Command2::set_vop_amplitude(&CMD2, 0x45)); + display.write_command(BK1Command2::set_vcom_amplitude(&CMD2, 0x13)); + display.write_command(BK1Command2::set_vgh_voltage(&CMD2, 0x07)); + display.write_command(BK1Command2::test_command_setting(&CMD2)); + display.write_command(BK1Command2::set_vgl_voltage(&CMD2, 0x07)); + display.write_command(BK1Command2::power_control_one( + &CMD2, + instructions::GammaOPBias::Middle, + instructions::SourceOPInput::Min, + instructions::SourceOPOutput::Off, + )); + + display.write_command(BK1Command2::power_control_two( + &CMD2, + instructions::VoltageAVDD::Pos6_6, + instructions::VoltageAVCL::Neg4_4, + )); + display.write_command(BK1Command2::set_pre_drive_timing_one(&CMD2, 0x03)); + display.write_command(BK1Command2::set_pre_drive_timing_two(&CMD2, 0x03)); + + // UNKNOWABLE CARGO-CULTED MYSTERY MEAT + // + // I copied this command sequence from the Linux MIPI driver for the ST7701, + // written in C. The author of that driver _also_ had no idea what this + // command sequence does or means, and claims to have himself copied it from + // a sample provided by a Sitronix engineer. Since this is a Linux SPI + // driver for the ST7701S written in Rust, there's ample opportunity for + // something to not line up. + // + // May whatever gods you pray to have mercy on our souls. + display.write_command(Ok(Command { + address: 0xE0, + parameters: vec![0x00, 0x00, 0x02], + })); + display.write_command(Ok(Command { + address: 0xE1, + parameters: vec![ + 0x0B, 0x00, 0x0D, 0x00, 0x0C, 0x00, 0x0E, 0x00, 0x00, 0x44, 0x44, + ], + })); + display.write_command(Ok(Command { + address: 0xE2, + parameters: vec![ + 0x33, 0x33, 0x44, 0x44, 0x64, 0x00, 0x66, 0x00, 0x65, 0x00, 0x67, 0x00, 0x00, + ], + })); + display.write_command(Ok(Command { + address: 0xE3, + parameters: vec![0x00, 0x00, 0x33, 0x33], + })); + display.write_command(Ok(Command { + address: 0xE4, + parameters: vec![0x44, 0x44], + })); + display.write_command(Ok(Command { + address: 0xE5, + parameters: vec![ + 0x0C, 0x78, 0x3C, 0xA0, 0x0E, 0x78, 0x3C, 0xA0, 0x10, 0x78, 0x3C, 0xA0, 0x12, 0x78, + 0x3C, 0xA0, + ], + })); + display.write_command(Ok(Command { + address: 0xE6, + parameters: vec![0x00, 0x00, 0x33, 0x33], + })); + display.write_command(Ok(Command { + address: 0xE7, + parameters: vec![0x44, 0x44], + })); + display.write_command(Ok(Command { + address: 0xE8, + parameters: vec![ + 0x0D, 0x78, 0x3C, 0xA0, 0x0F, 0x78, 0x3C, 0xA0, 0x11, 0x78, 0x3C, 0xA0, 0x13, 0x78, + 0x3C, 0xA0, + ], + })); + display.write_command(Ok(Command { + address: 0xEB, + parameters: vec![0x02, 0x02, 0x39, 0x39, 0xEE, 0x44, 0x00], + })); + display.write_command(Ok(Command { + address: 0xEC, + parameters: vec![0x00, 0x00], + })); + display.write_command(Ok(Command { + address: 0xED, + parameters: vec![ + 0xFF, 0xF1, 0x04, 0x56, 0x72, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF3, 0x27, 0x65, 0x40, + 0x1F, 0xFF, + ], + })); + + // BK1 COMMAND2 DISABLE + display.write_command(CommandsGeneral::set_command_2(Command2Selection::Disabled)); + CMD2 = Command2Selection::Disabled; + + // display.write_command(CommandsGeneral::set_color_mode( + // instructions::BitsPerPixel::Rgb666, + // )); + // display.write_command(CommandsGeneral::display_data_control( + // instructions::ScanDirection::Normal, + // instructions::ColorOrder::Rgb, + // )); + display.write_command(CommandsGeneral::tearing_effect_on( + instructions::TearingEffect::VHBlank, + )); + + display.write_command(CommandsGeneral::display_on()); + thread::sleep(time::Duration::from_millis(200)); +} diff --git a/src/panel.rs b/src/panel.rs new file mode 100644 index 0000000..ae126df --- /dev/null +++ b/src/panel.rs @@ -0,0 +1,84 @@ +pub struct Mode { + pub clock: u32, + + pub hdisplay: u32, + pub hsync_start: u32, + pub hsync_end: u32, + pub htotal: u32, + + pub vdisplay: u32, + pub vsync_start: u32, + pub vsync_end: u32, + pub vtotal: u32, + + pub width_mm: u32, + pub height_mm: u32, +} + +pub const DefaultMode: Mode = Mode { + clock: 27500, + + hdisplay: 480, + hsync_start: 480 + 38, + hsync_end: 480 + 38 + 12, + htotal: 480 + 38 + 12 + 12, + + vdisplay: 854, + vsync_start: 854 + 18, + vsync_end: 854 + 18 + 8, + vtotal: 854 + 18 + 8 + 4, + + width_mm: 69, + height_mm: 139, +}; + +pub const TDOMode: Mode = Mode { + clock: 16000, + + hdisplay: 480, + hsync_start: 480 + 24, + hsync_end: 480 + 24 + 6, + htotal: 480 + 24 + 6 + 18, + + vdisplay: 480, + vsync_start: 480 + 16, + vsync_end: 480 + 16 + 4, + vtotal: 480 + 16 + 4 + 10, + + width_mm: 69, + height_mm: 139, +}; + +pub const CVTMode: Mode = Mode { + clock: 17000, + + hdisplay: 480, + hsync_start: 480 + 8, + hsync_end: 480 + 8 + 48, + htotal: 480 + 8 + 48 + 56, + + vdisplay: 480, + vsync_start: 480 + 1, + vsync_end: 480 + 1 + 3, + vtotal: 480 + 1 + 3 + 13, + + width_mm: 69, + height_mm: 139, +}; + +pub const CVTRB: Mode = Mode { + clock: 23500, + + hdisplay: 640, + hsync_start: 480 + 8, + hsync_end: 480 + 8 + 32, + htotal: 480 + 8 + 32 + 40, + + vdisplay: 480, + vsync_start: 480 + 14, + vsync_end: 480 + 14 + 3, + vtotal: 480 + 14 + 3 + 4, + + width_mm: 69, + height_mm: 139, +}; diff --git a/src/spi.rs b/src/spi.rs new file mode 100644 index 0000000..49c1566 --- /dev/null +++ b/src/spi.rs @@ -0,0 +1,63 @@ +extern crate spidev; + +use crate::instructions::Command; +use spidev::{SpiModeFlags, Spidev, SpidevOptions}; +use std::io; +use std::io::prelude::*; + +pub struct ST7701S { + spi: Spidev, + options: SpidevOptions, +} + +impl ST7701S { + pub fn create_spi(device: String, options: &SpidevOptions) -> io::Result { + let mut spi = Spidev::open(device)?; + spi.configure(&options)?; + Ok(spi) + } + + pub fn new(device: String) -> ST7701S { + let options = SpidevOptions::new() + .bits_per_word(9) + .lsb_first(false) + .max_speed_hz(20_000) + .mode(SpiModeFlags::SPI_MODE_0) + .build(); + let spi = ST7701S::create_spi(device, &options).unwrap(); + + ST7701S { options, spi } + } + + pub fn write_command(&mut self, command: Result) -> Result<(), ()> { + match command { + Ok(c) => { + let address = &c.serialize_address(); + self.spi.write(&c.serialize_address()); + println!("address {}", c.address); + + for parameter in c.parameters { + self.spi.write(&Command::serialize_parameter(parameter)); + println!("parameter {}", parameter); + } + } + Err(e) => println!("{}", e), + } + + Ok(()) + } + + pub fn read_command(&mut self, command: Result) -> Result<(), ()> { + match command { + Ok(c) => { + let mut rx_buf = [0_u8; 10]; + self.spi.write(&c.serialize_address()); + self.spi.read(&mut rx_buf); + println!("{:?}", rx_buf); + } + Err(e) => println!("{}", e), + } + + Ok(()) + } +}