/* firmware to control LCD using HD44780 driver over I²C * Copyright (C) 2019-2020 King Kévin * SPDX-License-Identifier: GPL-3.0-or-later */ #include #include #include "stm8s.h" #include "main.h" // on-board LED pin in on PB5 (use as sink), same as SDA #define LED_PORT GPIO_PA #define LED_PIN PA3 #define led_on() {LED_PORT->ODR.reg &= ~LED_PIN;} #define led_off() {LED_PORT->ODR.reg |= LED_PIN;} #define led_toggle() {LED_PORT->ODR.reg ^= LED_PIN;} /* usual HD44780 pinout: * - 1 GND: ground * - 2 VCC: 5V (3.3V versions also exist, but a less common) * - 3 V0 : LCD bias voltage, connect to 10-20k potentiometer (VCC to GND) * - 4 RS : Register Select (high = data, low = instruction) * - 5 R/W: Read/Write (high = read, low = write) * - 6 E : Enable (falling edge to latch data, high to output register) * - 7 DB0: Data Bit 0 (for 8-bit transfer) * - 8 DB1: Data Bit 1 (for 8-bit transfer) * - 9 DB2: Data Bit 2 (for 8-bit transfer) * - 10 DB3: Data Bit 3 (for 8-bit transfer) * - 11 DB4: Data Bit 4 (for 4-bit transfer) * - 12 DB5: Data Bit 5 (for 4-bit transfer) * - 13 DB6: Data Bit 6 (for 4-bit transfer) * - 14 DB7: Data Bit 7 (for 4-bit transfer) * - 15 BLA: Backlight Anode * - 16 BLK: Backlight Cathode * * we use 4-bit mode since we are fast enough to send the whole data while receiving a byte, and this saves 4 I/Os */ #define HD44780_RS_PORT GPIO_PD #define HD44780_RS_PIN PD3 #define HD44780_RW_PORT GPIO_PD #define HD44780_RW_PIN PD2 #define HD44780_E_PORT GPIO_PC #define HD44780_E_PIN PC7 #define HD44780_DB4_PORT GPIO_PC #define HD44780_DB4_PIN PC6 #define HD44780_DB5_PORT GPIO_PC #define HD44780_DB5_PIN PC5 #define HD44780_DB6_PORT GPIO_PC #define HD44780_DB6_PIN PC4 #define HD44780_DB7_PORT GPIO_PC #define HD44780_DB7_PIN PC3 // the I²C address of this slave #define I2C_ADDR 0x28 // the functions we can call over I²C enum i2c_mode_t { // custom modes MODE_CLEAR_DISPLAY, // clear display MODE_LINE1, // write to line 1 MODE_LINE2, // write to line 2 MODE_DISPLAY_ON, // turn display on MODE_DISPLAY_OFF, // turn display off MODE_BRIGHTNESS, // set backlight brightness // raw instructions, directly mapping to HD44780 MODE_DISPLAY, MODE_RETURN_HOME, MODE_ENTRY_MODE_SET, MODE_CURSOR_DISPLAY_SHIFT, MODE_FUNCTION_SET, MODE_CGRAM_ADDR, MODE_DDRAM_ADDR, MODE_DATA, MODE_COUNT, // number of modes }; static enum i2c_mode_t i2c_mode = MODE_COUNT; // set invalid value /* actually we are fast enough to process bytes as they are received, even in 4-bit mode, except when the clear display instruction is used */ static volatile uint8_t i2c_input_buffer[2 * 20 + 1] = {0}; /**< ring buffer for received data (enough for two lines) */ static volatile uint8_t i2c_input_i = 0; /**< current position of read received data */ static volatile uint8_t i2c_input_used = 0; /**< how much data has been received and not read */ static volatile bool i2c_input_new = false; /**< if a transaction with new data started */ // Look-Up Table is faster than doing the calculation static const uint8_t nibble_reverse_bit_order_lut[] = { 0x0, 0x8, 0x4, 0xc, 0x2, 0xa, 0x6, 0xe, 0x1, 0x9, 0x5, 0xd, 0x3, 0xb, 0x7, 0xf, }; /** * @warning only up to UINT32_MAX / 10 to be safe */ static void wait_10us(uint32_t us10) { us10 = ((us10 / (1 << CLK->CKDIVR.fields.HSIDIV)) * 1000) / 206; // calibrated for 1 ms while (us10--); // burn energy } static void hd44780_data_direction(bool read) { if (read) { // switch data pins to input, with pull-up (should already be on the LCD module) // this is specific to the port definition, optimized here for speed PC_DDR &= ~(PC3 | PC4 | PC5 | PC6); // switch data pins to input HD44780_RW_PORT->ODR.reg |= HD44780_RW_PIN; // set high to read while (!(HD44780_RW_PORT->IDR.reg & HD44780_RW_PIN)); // wait for RW to be high } else { HD44780_RW_PORT->ODR.reg &= ~HD44780_RW_PIN; // set low to write // this is specific to the port definition, optimized here for speed PC_DDR |= (PC3 | PC4 | PC5 | PC6); // switch data pins to output while ((HD44780_RW_PORT->IDR.reg & HD44780_RW_PIN)); // wait for RW to be low } } /** * @note the direction and instruction/data should already be set */ static void hd44780_write_nibble(uint8_t nibble) { HD44780_E_PORT->ODR.reg |= HD44780_E_PIN; // set enable high so we can change the data nibble = nibble_reverse_bit_order_lut[nibble & 0x0f]; // reverse bit order to match pins nibble = nibble << 3; // set IO according to nibble PC_ODR = ((PC_ODR & 0x87) | nibble); // set IO according to nibble // wait t_DSW = 195 ns or PW_EH = 450 ns HD44780_E_PORT->ODR.reg &= ~HD44780_E_PIN; // set enable low to latch data // no need to wait t_H = 10 ns before next step since next instructions are slower } /** * @note the direction and instruction/data should already be set */ static uint8_t hd44780_read_nibble(void) { HD44780_E_PORT->ODR.reg |= HD44780_E_PIN; // set enable to have data output // wait t_DDR = 360 ns for the data to be output __asm nop nop nop nop nop __endasm; uint8_t nibble = ((PC_IDR >> 3) & 0x0f); // read DB7-DB4 // no need to wait PW_EH = 450 ns HD44780_E_PORT->ODR.reg &= ~HD44780_E_PIN; // set enable low end read return nibble_reverse_bit_order_lut[nibble]; } static uint8_t hd44780_read_byte(void) { hd44780_data_direction(true); // switch to read direction uint8_t data = (hd44780_read_nibble() << 4); // get first nibble // no need to wait t_cycE = 500 ns before next write data |= hd44780_read_nibble(); // get second nibble // no need to wait tAS = 40 ns before next step since the instructions are slower return data; } static uint8_t hd44780_read_bfac(void) { HD44780_RS_PORT->ODR.reg &= ~HD44780_RS_PIN; // set low for instruction return hd44780_read_byte(); } /** * @note instruction/data should already be set */ static void hd44780_write_byte(uint8_t data) { hd44780_data_direction(false); // switch to write direction // no need to wait tAS = 40 ns before next step since the instructions are slower hd44780_write_nibble(data >> 4); // send first nibble // no need to wait t_cycE = 500 ns before next write hd44780_write_nibble(data); // send second nibble // no need to wait t_cycE = 500 ns before next write } static void hd44780_write_instruction(uint8_t instruction) { while (hd44780_read_bfac() & 0x80); // wait until busy flag is cleared HD44780_RS_PORT->ODR.reg &= ~HD44780_RS_PIN; // set low for instruction hd44780_write_byte(instruction); } static void hd44780_write_data(uint8_t data) { while (hd44780_read_bfac() & 0x80); // wait until busy flag is cleared HD44780_RS_PORT->ODR.reg |= HD44780_RS_PIN; // set high for data hd44780_write_byte(data); } void main(void) { sim(); // disable interrupts (while we reconfigure them) CLK->CKDIVR.fields.HSIDIV = CLK_CKDIVR_HSIDIV_DIV0; // don't divide high speed internal (HSI) 16 MHz clock, we need it to process the data fast enough // HSI clock is used for as master clock per default CLK->CKDIVR.fields.CPUDIV = CLK_CKDIVR_CPUDIV_DIV0; // don't divide CPU frequency for now (will be master clock) while (!(CLK_ICKR & CLK_ICKR_HSIRDY)); // wait for internal oscillator to be ready // save power by disabling unused peripheral CLK_PCKENR1 = CLK_PCKENR1_I2C; // only keep I²C CLK_PCKENR2 = CLK_PCKENR2_AWU; // only keep AWU // configure LED LED_PORT->DDR.reg |= LED_PIN; // switch pin to output LED_PORT->CR1.reg &= ~LED_PIN; // use in open-drain mode led_off(); // start with LED off // configure independent watchdog (very loose, just it case the firmware hangs) IWDG_KR = IWDG_KR_KEY_REFRESH; // reset watchdog IWDG_KR = IWDG_KR_KEY_ENABLE; // start watchdog IWDG_KR = IWDG_KR_KEY_ACCESS; // allows changing the prescale IWDG->PR.fields.PR = IWDG_PR_DIV256; // set prescale to longest time (1.02s) IWDG_KR = IWDG_KR_KEY_REFRESH; // reset watchdog /* configure HD44780 pins * the display is a lot more stable the operated in push-pull mode * but they can also be operated in open-drain mode (as does the PCF8574), * since all pins are pulled up by the HD44780 , except E which requires an external pull-up resistor (~1 kO). * when operated in open-drain mode, wait the recommended maximum operation times since reading is error prone */ HD44780_RS_PORT->DDR.reg |= HD44780_RS_PIN; // switch pin to output HD44780_RS_PORT->CR1.reg &= ~HD44780_RS_PIN; // use in open-drain mode HD44780_RS_PORT->CR1.reg |= HD44780_RS_PIN; // use in push-pull mode HD44780_RW_PORT->DDR.reg |= HD44780_RW_PIN; // switch pin to output HD44780_RW_PORT->CR1.reg &= ~HD44780_RW_PIN; // use in open-drain mode HD44780_RW_PORT->CR1.reg |= HD44780_RW_PIN; // use in push-pull mode HD44780_E_PORT->DDR.reg |= HD44780_E_PIN; // switch pin to output HD44780_E_PORT->CR1.reg |= HD44780_E_PIN; // use in push-pull mode HD44780_E_PORT->ODR.reg &= ~HD44780_E_PIN; // start idle low HD44780_DB7_PORT->DDR.reg |= HD44780_DB7_PIN; // switch pin to output HD44780_DB7_PORT->CR1.reg &= ~HD44780_DB7_PIN; // use in open-drain mode HD44780_DB7_PORT->CR1.reg |= HD44780_DB7_PIN; // use in push-pull mode HD44780_DB6_PORT->DDR.reg |= HD44780_DB6_PIN; // switch pin to output HD44780_DB6_PORT->CR1.reg &= ~HD44780_DB6_PIN; // use in open-drain mode HD44780_DB6_PORT->CR1.reg |= HD44780_DB6_PIN; // use in push-pull mode HD44780_DB5_PORT->DDR.reg |= HD44780_DB5_PIN; // switch pin to output HD44780_DB5_PORT->CR1.reg &= ~HD44780_DB5_PIN; // use in open-drain mode HD44780_DB5_PORT->CR1.reg |= HD44780_DB5_PIN; // use in push-pull mode HD44780_DB4_PORT->DDR.reg |= HD44780_DB4_PIN; // switch pin to output HD44780_DB4_PORT->CR1.reg &= ~HD44780_DB4_PIN; // use in open-drain mode HD44780_DB4_PORT->CR1.reg |= HD44780_DB4_PIN; // use in push-pull mode hd44780_data_direction(false); // configure pins as output // configure I²C GPIO_PB->CR1.reg |= (PB4 | PB5); // enable internal pull-up on SCL/SDA GPIO_PB->DDR.reg &= ~(PB4 | PB5); // set SCL/SDA as input before it is used as alternate function by the peripheral I2C_CR1 |= I2C_CR1_PE; // enable I²C peripheral (must be done before any other register is written) I2C_CR2 |= I2C_CR2_STOP; // release lines I2C_CR2 |= I2C_CR2_SWRST; // reset peripheral, in case we got stuck and the dog bit while (0 == (GPIO_PB->IDR.reg & PB4)); // wait for SCL line to be released while (0 == (GPIO_PB->IDR.reg & PB5)); // wait for SDA line to be released I2C_CR2 &= ~I2C_CR2_SWRST; // release reset I2C_CR1 |= I2C_CR1_ENGC; // enable general call I2C_CR1 |= I2C_CR1_PE; // re-enable I²C peripheral I2C_FREQR = 16; // the peripheral frequency is 4 MHz (must match CPU frequency) //I2C_CR1 |= I2C_CR1_ENGC; // enable general call (I was not able to have slave select with address 0x00 ACKed) I2C_CR2 |= I2C_CR2_ACK; // enable acknowledgement if address matches // since we are slave and not master, we don't have to set CCR I2C_OARL = (I2C_ADDR << 1); // set slave address I2C_ITR |= (I2C_ITR_ITBUFEN | I2C_ITR_ITEVTEN); // enable buffer and event interrupts // configure auto-wakeup (AWU) to be able to refresh the watchdog // 128 kHz LSI used by default in option bytes CKAWUSEL // we skip measuring the LS clock frequency since there is no need to be precise AWU->TBR.fields.AWUTB = 10; // interval range: 128-256 ms AWU->APR.fields.APR = 0x3e; // set time to 256 ms AWU_CSR |= AWU_CSR_AWUEN; // enable AWU (start only when entering wait or active halt mode) // configure display (as per datasheet) IWDG_KR = IWDG_KR_KEY_REFRESH; // reset watchdog HD44780_RS_PORT->ODR.reg &= ~HD44780_RS_PIN; // set low for instruction hd44780_data_direction(false); // switch to write direction wait_10us(4000 + 1000); // wait 40 ms after power up hd44780_write_nibble(3); // 1st function write set to go to state 1 (8-bit) or 2 (4-bit first nibble) (BF cannot be checked) wait_10us(410 + 100); // wait 4.1 ms hd44780_write_nibble(3); // 2st function write set to go to state 1 (8-bit) or 3 (4-bit second nibble) (BF cannot be checked) wait_10us(10 + 1); // wait 100 us hd44780_write_nibble(3); // 3rd function write set to go to state 1 (8-bit) (BF cannot be checked) wait_10us(4 + 1); // wait 37 us hd44780_write_nibble(2); // switch to 4-bit mode wait_10us(4 + 1); // wait 37 us (BF could be checked at this point) // we are now for sure in 8-bit more (and could switch do 4-bit). 8-bit mode is actually the default after power up IWDG_KR = IWDG_KR_KEY_REFRESH; // reset watchdog hd44780_write_instruction(0x28); // function set (DL=1: 4-bit mode, N=1: 2 lines, F=0: 5x8 dots) hd44780_write_instruction(0x08); // display off hd44780_write_instruction(0x01); // display clear hd44780_write_instruction(0x06); // entry mode set rim(); // re-enable interrupts led_on(); while (true) { IWDG_KR = IWDG_KR_KEY_REFRESH; // reset watchdog //led_toggle(); // indicate we re running while (i2c_input_used) { // I²C data is available sim(); // disable interrupt while reading buffer to not corrupt indexes uint8_t input_data = i2c_input_buffer[i2c_input_i]; // get start buffered data i2c_input_i = (i2c_input_i + 1) % ARRAY_LENGTH(i2c_input_buffer); // update used buffer i2c_input_used--; // update used buffer rim(); // re-enable interrupts if (i2c_input_new) { // this is the start of a transaction, the first byte indicates the mode to be used i2c_mode = input_data; // set user provided mode (no need to check since the undefined modes don't do anything) i2c_input_new = false; // clear flag // process mode switch switch (i2c_mode) { case MODE_CLEAR_DISPLAY: // clear display hd44780_write_instruction(0x01); // clear display instruction break; case MODE_LINE1: hd44780_write_instruction(0x80); // set DDRAM address to 0 (line 1) break; case MODE_LINE2: hd44780_write_instruction(0xc0); // set DDRAM address to 0x40 (line 2) break; case MODE_DISPLAY_ON: hd44780_write_instruction(0x0c); // display on break; case MODE_DISPLAY_OFF: hd44780_write_instruction(0x08); // display off break; case MODE_RETURN_HOME: hd44780_write_instruction(0x02); // return home break; default: break; // waiting for data } } else { // process data // note set RS at every byte since read busy always switches it to instruction switch (i2c_mode) { case MODE_LINE1: case MODE_LINE2: case MODE_DATA: hd44780_write_data(input_data); break; case MODE_ENTRY_MODE_SET: hd44780_write_instruction(0x04 | (input_data & 0x3)); break; case MODE_DISPLAY: hd44780_write_instruction(0x08 | (input_data & 0x7)); break; case MODE_CURSOR_DISPLAY_SHIFT: hd44780_write_instruction(0x10 | (input_data & 0xc)); break; case MODE_FUNCTION_SET: hd44780_write_instruction(0x20 | (input_data & 0xc)); // keep DL=0 4-bit mode break; case MODE_CGRAM_ADDR: hd44780_write_instruction(0x40 | (input_data & 0x3f)); break; case MODE_DDRAM_ADDR: hd44780_write_instruction(0x80 | (input_data & 0x3f)); break; case MODE_BRIGHTNESS: if (input_data) { led_on(); } else { led_off(); } break; default: // read values do not make sense break; } } } //for (volatile uint32_t wait = 0; wait < 100000; wait++); wfi(); // go to wait mode (halt would prevent slave select to be acknowledged) } } void awu(void) __interrupt(IRQ_AWU) // auto wakeup { volatile uint8_t awuf = AWU_CSR; // clear interrupt flag by reading it (reading is required, and volatile prevents compiler optimization) // let the main loop kick the dog } void i2c(void) __interrupt(IRQ_I2C) // auto wakeup { // make copies of status registers, since some bits might be cleared meanwhile uint8_t sr1 = I2C_SR1; uint8_t sr2 = I2C_SR2; uint8_t sr3 = I2C_SR3; if (sr1 & I2C_SR1_TXE) { // transmission buffer is empty I2C_DR = 0xff; // read is not a valid command, return anything } if (sr1 & I2C_SR1_RXNE) { // receive buffer is full uint8_t data = I2C_DR; // read data (also clears flag); if (i2c_input_used < ARRAY_LENGTH(i2c_input_buffer)) { // only store when there is place, else drop new data i2c_input_buffer[(i2c_input_i + i2c_input_used) % ARRAY_LENGTH(i2c_input_buffer)] = data; // save new data i2c_input_used++; // update used buffer } } if (sr1 & I2C_SR1_STOPF) { // stop received I2C_CR2 |= I2C_CR2_ACK; // this is just to clear the flag } if (sr1 & I2C_SR1_ADDR) { // our slave address has been selected if (sr3 & I2C_SR3_TRA) { // we will have to transmit data } else { // we will receive data i2c_input_new = true; // informs a new transaction started } } if (sr1 & I2C_SR1_BTF) { // byte transfer finished (only set when stretching has been enabled) // cleared by reading/writing from/to DR or when stop is received } if (sr2 & I2C_SR2_AF) { // NACK received (e.g. end of read transaction) I2C_SR2 &= ~I2C_SR2_AF; // clear flag } }