main: import code from 2020-02-13

This commit is contained in:
King Kévin 2022-06-22 12:41:24 +02:00
parent be90d9a7d1
commit 57c1be26d3
1 changed files with 419 additions and 21 deletions

440
main.c
View File

@ -1,30 +1,349 @@
/* firmware template for STM8S microcontroller
/* firmware to control LCD using HD44780 driver over I²C
* Copyright (C) 2019-2020 King Kévin <kingkevin@cuvoodoo.info>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include "stm8s.h"
#include "main.h"
// get length of array
#define ARRAY_LENGTH(x) (sizeof(x) / sizeof((x)[0]))
// 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;}
// blocking wait (in 10 us steps, up to UINT32_MAX / 10)
/* 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
enum i2c_mode_t {
MODE_INSTRUCTION, // read/write instruction/command
MODE_DATA, // read/write data
MODE_PORT, // read/write
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,
};
/*
struct settings_t {
uint8_t i2c_slave_addr;
} settings;
// start address of EEPROM
#define EEPROM_ADDR 0x4000
void load_settings(void)
{
uint8_t* eeprom = (uint8_t*)EEPROM_ADDR; // address where the settings are saved
// check if magic header is present
if (0x42 != *(eeprom + 0)) {
goto error;
}
// check if checksum is correct
uint8_t crc = 0;
for (uint8_t i = 0; i < sizeof(struct settings_t) + 2; i++) {
crc ^= *(eeprom + i);
}
if (0 != crc) {
goto error;
}
// now we can load the settings
for (uint8_t i = 0; i < sizeof(struct settings_t); i++) {
*(((uint8_t*)settings) + 0) = *(eeprom + 1 + i);
}
return;
error: // use default in case of error
settings.i2c_slave_addr = 0x28;
}
bool save_settings(void)
{
uint8_t* eeprom = (uint8_t*)EEPROM_ADDR; // address where the settings are saved
uint8_t i; // index variable
bool to_return = false; // if settings have been saved
if (!(FLASH_IAPSR & FLASH_IAPSR_DUL)) { // write protection is enabled
// disable DATA (e.g. EEPROM) write protection
FLASH_DUKR = FLASH_DUKR_KEY1;
FLASH_DUKR = FLASH_DUKR_KEY2;
if (!(FLASH_IAPSR & FLASH_IAPSR_DUL)) { // un-protecting failed
return false;
}
}
// write magic header
if (0x42 != *(eeprom + 0)) {
*(eeprom + 0) = 0x42;
if (!(FLASH_IAPSR & FLASH_IAPSR_EOP)) { // write failed
goto end;
}
}
// write settings
for (i = 0; i < sizeof(struct settings_t); i++) {
if (*(eeprom + 1 + i) != *(((uint8_t*)settings) + 0)) {
*(eeprom + 1 + i) = *(((uint8_t*)settings) + 0);
if (!(FLASH_IAPSR & FLASH_IAPSR_EOP)) { // write failed
goto end;
}
}
}
// write checksum
uint8_t crc = 0;
for (i = 0; i < 1 + sizeof(struct settings_t); i++) {
crc ^= *(eeprom + i);
}
if (*(eeprom + 1 + sizeof(struct settings_t)) != crc) {
*(eeprom + 1 + sizeof(struct settings_t)) = crc;
if (!(FLASH_IAPSR & FLASH_IAPSR_EOP)) { // write failed
goto end;
}
}
to_return = true; // saving settings succeeded
end:
FLASH_IAPSR &= ~FLASH_IAPSR_DUL; // re-enable write protection
return to_return;
}
*/
/**
* @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
for (volatile uint32_t t = 0; t < us10; t++); // 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 internal 16 MHz clock
CLK->CKDIVR.fields.CPUDIV = CLK_CKDIVR_CPUDIV_DIV0; // don't divide CPU frequency to 16 MHz
while (!CLK->ICKR.fields.HSIRDY); // wait for internal oscillator to be ready
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
@ -33,22 +352,68 @@ void main(void)
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 independent watchdog (very loose, just it case the firmware hangs)
IWDG->KR.fields.KEY = IWDG_KR_KEY_REFRESH; // reset watchdog
IWDG->KR.fields.KEY = IWDG_KR_KEY_ENABLE; // start watchdog
IWDG->KR.fields.KEY = IWDG_KR_KEY_ACCESS; // allows changing the prescale
IWDG->PR.fields.PR = IWDG_PR_DIV256; // set prescale to longest time (1.02s)
IWDG->KR.fields.KEY = IWDG_KR_KEY_REFRESH; // reset watchdog
// 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(0x20); // function set: 4-bit mode
hd44780_write_instruction(0x08); // display off
hd44780_write_instruction(0x01); // display clear
hd44780_write_instruction(0x06); // entry mode set
rim(); // re-enable interrupts
bool action = false; // if an action has been performed
led_on();
while (true) {
IWDG_KR = IWDG_KR_KEY_REFRESH; // reset watchdog
if (action) { // something has been performed, check if other flags have been set meanwhile
action = false; // clear flag
} else { // nothing down
wfi(); // go to sleep (wait for any interrupt, including periodic AWU)
//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
} else {
// process data
// note set RS at every byte since read busy always switches it to instruction
switch (i2c_mode) {
case MODE_PORT:
if (input_data) {
led_on();
} else {
led_off();
}
break;
case MODE_INSTRUCTION:
if (0x20 == (input_data & 0x20)) { // function set instruction
input_data &= ~(1 << 4); // ensure we stay in 4-bit mode
}
hd44780_write_instruction(input_data);
break;
case MODE_DATA:
hd44780_write_data(input_data);
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)
}
}
@ -57,3 +422,36 @@ 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
}
}