stm8s/main.c

232 lines
8.4 KiB
C

/* firmware template for STM8S microcontroller
* Copyright (C) 2019-2022 King Kévin <kingkevin@cuvoodoo.info>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include "stm8s.h"
#include "main.h"
#define EEPROM_ADDR 0x4000 // EEPROM start address
// LED pin (sink for on)
#define LED_PORT GPIO_PA
#define LED_PIN PA3
// pull ups for device facing I²C port
#define SDA_PU_PORT GPIO_PC
#define SDA_PU_PIN PC3
#define SCL_PU_PORT GPIO_PC
#define SCL_PU_PIN PC4
// external EEPROM write protect pin
#define WP_PORT GPIO_PC
#define WP_PIN PC5
// hot plug detect pull up
#define HPD_PORT GPIO_PC
#define HPD_PIN PC6
// copy EDID setting
#define EDID_PORT GPIO_PC
#define EDID_PIN PC7
// if an I²C transaction started
static volatile bool i2c_transaction_new = false;
// if an I²C transaction with new data started
static volatile bool i2c_input_new = false;
// blocking wait (in 10 us steps, up to UINT32_MAX / 10)
static void wait_10us(uint32_t us10)
{
us10 = ((us10 / (1 << CLK->CKDIVR.fields.HSIDIV)) * 1000) / 206; // calibrated for 1 ms
while (us10--); // burn energy
}
void putc(char c)
{
while (!UART1->SR.fields.TXE); // wait until TX buffer is empty
UART1->DR.reg = c; // put character in buffer to be transmitted
// don't wait until the transmission is complete
}
void puts(const char* s)
{
if (NULL == s) {
return;
}
while (*s) {
putc(*s++);
IWDG_KR = IWDG_KR_KEY_REFRESH; // reset watchdog
}
}
void putn(uint8_t n)
{
n &= 0x0f; // ensure it's a nibble
if (n < 0xa) {
putc('0' + n);
} else {
putc('a' + (n - 0x0a));
}
}
void puth(uint8_t h)
{
putn(h >> 4);
putn(h & 0x0f);
}
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
// configure LED
LED_PORT->DDR.reg |= LED_PIN; // switch pin to output
LED_PORT->CR1.reg &= ~LED_PIN; // use in open-drain mode
LED_PORT->ODR.reg |= LED_PIN; // switch LED off
// 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 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 UART for debug output
UART1->CR1.fields.M = 0; // 8 data bits
UART1->CR3.fields.STOP = 0; // 1 stop bit
UART1->BRR2.reg = 0x0B; // set baud rate to 115200 (at 16 MHz)
UART1->BRR1.reg = 0x08; // set baud rate to 115200 (at 16 MHz)
UART1->CR2.fields.TEN = 1; // enable TX
// configure I²C pull-ups
SDA_PU_PORT->DDR.reg |= SDA_PU_PIN; // switch pin to output
SDA_PU_PORT->CR1.reg |= SDA_PU_PIN; // switch pin to push pull
SDA_PU_PORT->ODR.reg |= SDA_PU_PIN; // pull up SDA line
SCL_PU_PORT->DDR.reg |= SCL_PU_PIN; // switch pin to output
SCL_PU_PORT->CR1.reg |= SCL_PU_PIN; // switch pin to push pull
SCL_PU_PORT->ODR.reg |= SCL_PU_PIN; // pull up SCL line
// TODO verify if I²C lines are connected to monitor
// configure external EEPROM (actually not used)
WP_PORT->DDR.reg |= WP_PIN; // switch pin to output
WP_PORT->CR1.reg &= ~WP_PIN; // switch pin to open drain
WP_PORT->ODR.reg |= WP_PIN; // let write protect pulled up
// 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_PE; // re-enable I²C peripheral
I2C_FREQR = 16; // the peripheral frequency is 4 MHz (must match CPU frequency)
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 = (0x50U << 1U); // set slave address for EEPROM
I2C_ITR |= (I2C_ITR_ITBUFEN | I2C_ITR_ITEVTEN); // enable buffer and event interrupts
// configure hot plug detect
HPD_PORT->DDR.reg |= HPD_PIN; // switch pin to output
HPD_PORT->CR1.reg |= HPD_PIN; // switch pin to push pull
HPD_PORT->ODR.reg |= HPD_PIN; // pull up HPD line to indicate EDID is ready
// TODO check if connected to sink/monitor
/*
The Sink shall be capable of responding with EDID 1.3 data and up to 255 extension blocks, each
128 bytes long (up to 32K bytes total E-EDID memory) whenever the Hot Plug Detect signal is
asserted.
The Sink should be capable of providing E-EDID information over the Enhanced DDC channel
whenever the +5V Power signal is provided. This should be available within 20msec after the +5V
Power signal is provided.
*/
// check if EDID should be copied
EDID_PORT->DDR.reg &= ~EDID_PIN; // switch pin to output
EDID_PORT->CR1.reg |= EDID_PIN; // enable pull up
if (0 == (EDID_PORT->ODR.reg & EDID_PIN)) { // EDID switched on
// TODO copy EDID from sink/monitor
/*
An HDMI Sink shall indicate any change to the contents of the E-EDID by driving a low voltage
level pulse on the Hot Plug Detect pin. This pulse shall be at least 100 msec.
*/
}
rim(); // re-enable interrupts
bool action = false; // if an action has been performed
LED_PORT->ODR.reg &= ~LED_PIN; // switch LED on to indicate we are ready
puts("ready\r\n");
while (true) {
IWDG_KR = IWDG_KR_KEY_REFRESH; // reset watchdog
if (i2c_transaction_new) { // an I²C transaction started
i2c_transaction_new = false; // clear flag
if (i2c_input_new) {
puts("I²C write\r\n");
} else {
puts("I²C read\r\n");
}
}
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)
}
}
}
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
{
static uint8_t addr = 0; // EEPROM address to read
// make copies of status registers, since some bits might be cleared meanwhile
const uint8_t sr1 = I2C_SR1;
const uint8_t sr2 = I2C_SR2;
const uint8_t sr3 = I2C_SR3; // clears ADDR after reading SR1
if (sr1 & I2C_SR1_TXE) { // transmission buffer is empty
I2C_DR = *(uint8_t*)(EEPROM_ADDR + addr++); // transmit next byte
}
if (sr1 & I2C_SR1_RXNE) { // receive buffer is full
const uint8_t data = I2C_DR; // read data (also clears flag)
if (I2C_CR2 & I2C_CR2_ACK) {
addr = data; // we only take the first address byte
}
I2C_CR2 &= I2C_CR2_ACK; // NACK next received byte
}
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
i2c_transaction_new = true; // notify main loop transaction started
if (sr3 & I2C_SR3_TRA) { // start data transmission
I2C_DR = *(uint8_t*)(EEPROM_ADDR + addr++); // transmit selected byte
i2c_input_new = false; // notify we send data
} else { // we will receive data
I2C_CR2 |= I2C_CR2_ACK; // ACK next received byte
i2c_input_new = true; // notify we get data
}
}
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
}
}