/* * l9779.cpp * * Created on: Jan 10, 2022 * * Andrey Gusakov, (c) 2022 * * Masks/inputs bits: * 0..3 - IGN1 .. 4 - Ignition pre-drivers * Driven by logical-AND of SPI control bit and dedicated parallel input IGNI1...IGNI4 * 4..7 - OUT1 .. 4 - Protected low-side drivers with max current 2.2A * Driven by logical-AND of SPI control bit and dedicated parallel input IN1...IN4 * 8 - OUT5 - Protected low-side driver with max current 3A * Driven by logical-AND of SPI control bit and dedicated parallel input IN5 * 9..10 - OUT6,7 - Protected low-side drivers with max current 5A (O2 heaters) * Driven by logical-AND of SPI control bit and dedicated parallel input IN6, IN7. * 11 - Unused (no OUT8), IN8-PWM is used for stepper * 12..15 - OUT9..12 - Not exist on L9779WD-SPI, TODO: check L9779WD * 16..17 - OUT13..14 - Protected low side relay drivers with max current 600 mA and Low Battery Volatage function * 18..21 - OUT15..18 - Protected low side relay drivers with max current 600 mA * 22 - Unused (no OUT19) * 23 - OUT20 - Protected low side low current driver with max current 50 mA * 24..27 - OUTA..D - Configurable outputs (OD, PP) with max current 0.6 A (for low and high side FETs) * Can be configured for stepper motor driving. * Stepper is controlled by the logic AND between PWM (IN8) input pin and PWM SPI bit. * 28..31 - OUT25..27 - Unused on L9779WD-SPI, TODO for L9779WD * 32 - MR - Main Relay low side driver with max current 0.6 A, automaticly controlled */ #include "pch.h" #include "gpio/l9779.h" #if (BOARD_L9779_COUNT > 0) #include "persistent_configuration.h" #include "hardware.h" #include "gpio/gpio_ext.h" /* * TODO list: * - just write code */ /*==========================================================================*/ /* Driver local definitions. */ /*==========================================================================*/ #define DRIVER_NAME "l9779" #define DIAG_PERIOD_MS (7) typedef enum { L9779_DISABLED = 0, L9779_WAIT_INIT, L9779_READY, L9779_FAILED } l9779_drv_state; /* SPI communication helpers */ /* Out frame */ /* D0 - parity */ /* D8:D1 - DATA OUT or SUBADDRESS if ADD[4:0] = 0x10 (for read) */ #define MSG_SET_DATA(d) (((d) & 0xff) << 1) /* sub-address is 5 bit */ #define MSG_SET_SUBADDR(s) (((s) & 0x1f) << 1) /* D9 - x */ /* D14:D10 - ADDRESS */ #define MSG_SET_ADDR(a) (((a) & 0x1f) << 10) /* D15 - x */ /* ADD user for read commands */ #define MSG_READ_ADDR (0x10) #define MSG_W(a, d) (static_cast((MSG_SET_ADDR(a) | MSG_SET_DATA(d)))) #define MSG_R(a) (static_cast((MSG_SET_ADDR(MSG_READ_ADDR) | MSG_SET_SUBADDR(d)))) /* Both DIN and DO */ /* D0 - parity */ #define MSG_GET_PARITY(x) (((x) >> 0) & 0x01) /* D14:D10 - Addr of DATA IN or DATA OUT */ #define MSG_GET_ADDR(x) (((x) >> 10) & 0x1f) /* D8:D1 - DATA IN */ #define MSG_GET_DATA(x) (((x) >> 1) & 0xff) /* DIN / to chip */ /* D8:D1 or 5 bits of subaddr in case of read access */ #define MSG_GET_SUBADDR(tx) (MSG_GET_DATA(tx) & 0x1f) /* DOUT / from chip */ /* D 9 - W/R flag, 1 if we read */ #define MSG_GET_WR(rx) (((rx) >> 9) & 0x01) /* D15 - SPI error flag */ #define MSG_GET_SPIERROR(rx) (((rx) >> 15) & 0x01) /* register address that never can be replyed */ #define REG_INVALID 0xff /* Write only registers */ #define CMD_CLOCK_UNLOCK_SW_RST(d) MSG_W(0x0c, (d)) #define CMD_START_REACT(d) MSG_W(0x0d, (d)) #define CMD_CONTR_REG(n, d) MSG_W(0x08 + (n), (d)) /* Read only registers */ /* IGN1..4 + OUT1..7 */ #define OUT_DIRECT_DRIVE_MASK 0x7ff /*==========================================================================*/ /* Driver exported variables. */ /*==========================================================================*/ /*==========================================================================*/ /* Driver local variables and types. */ /*==========================================================================*/ /* Driver private data */ struct L9779 : public GpioChip { int init() override; int deinit() override; int setPadMode(size_t pin, iomode_t mode) override; int writePad(size_t pin, int value) override; int readPad(size_t pin) override; brain_pin_diag_e getDiag(size_t pin) override; bool spi_parity_odd(uint16_t x); int spi_validate(uint16_t rx); int spi_rw(uint16_t tx, uint16_t *rx_ptr); int spi_rw_array(const uint16_t *tx, uint16_t *rx, int n); int update_output(); int update_direct_output(size_t pin, int value); int wake_driver(); int chip_reset(); int chip_init_data(); int chip_init(); brain_pin_diag_e getOutputDiag(size_t pin); brain_pin_diag_e getInputDiag(size_t pin); const l9779_config *cfg; /* thread stuff */ thread_t *thread; THD_WORKING_AREA(thread_wa, 256); semaphore_t wake; /* state to be sent to chip */ uint32_t o_state; /* output enabled mask */ uint32_t o_oe_mask; /* cached output registers state - value last send to chip */ uint32_t o_data_cached; l9779_drv_state drv_state; /* last accesed register */ uint8_t last_addr; /* last requested subaddr in case of read */ uint8_t last_subaddr; /* chip needs reintialization due to some critical issue */ bool need_init; /* statistic */ //int por_cnt; //int wdr_cnt; //int comfe_cnt; int init_cnt; //int init_req_cnt; int spi_cnt; int spi_err_parity; /* parity errors in rx data */ int spi_err_frame; /* rx messages with bit 15 set */ int spi_err; /* rx messages with incorrect ADDR or WR fields */ uint16_t tx; uint16_t rx; }; static L9779 chips[BOARD_L9779_COUNT]; static const char* l9779_pin_names[L9779_SIGNALS] = { "L9779.IGN1", "L9779.IGN2", "L9779.IGN3", "L9779.IGN4", "L9779.OUT1", "L9779.OUT2", "L9779.OUT3", "L9779.OUT4", "L9779.OUT5", "L9779.OUT6", "L9779.OUT7", "L9779.OUT8", "L9779.OUT9", "L9779.OUT10", "L9779.OUT11", "L9779.OUT12", "L9779.OUT13", "L9779.OUT14", "L9779.OUT15", "L9779.OUT16", "L9779.OUT17", "L9779.OUT18", "L9779.OUT19", "L9779.OUT20", "L9779.OUTA", "L9779.OUTB", "L9779.OUTC", "L9779.OUTD", "L9779.OUT25", "L9779.OUT26", "L9779.OUT27", "L9779.OUT28", "L9779.MRD", "L9779.KEY" }; /*==========================================================================*/ /* Driver local functions. */ /*==========================================================================*/ /* true if parity of input x is odd */ bool L9779::spi_parity_odd(uint16_t x) { x ^= x >> 8; x ^= x >> 4; x ^= x >> 2; x ^= x >> 1; return (x & 1); } int L9779::spi_validate(uint16_t rx) { if (!spi_parity_odd(rx)) { spi_err_parity++; return -1; } if (MSG_GET_SPIERROR(rx)) { /* not clear what does this means */ spi_err_frame++; return -1; } /* check that correct register is returned */ if (last_subaddr != REG_INVALID) { /* MISO DO returns 1 at D9 bit and 5bit sub address in * ADD[4:0] field */ if (!MSG_GET_WR(rx)) { return -2; } if (MSG_GET_ADDR(rx) != last_subaddr) { /* unexpected SPI answer */ spi_err++; /* should ve restart? */ //need_init = true; return -1; } } /* LOCK_UNLOCK_SW_RST */ if (last_addr == 0x0c) { /* BIT(0) = LOCK flag */ /* START_REACT */ } else if (last_addr == 0x0d) { /* BIT(0) = OUT_DIS */ } return 0; } /** * @returns -1 in case of communication error */ int L9779::spi_rw(uint16_t tx, uint16_t *rx_ptr) { int ret; uint16_t rx; SPIDriver *spi = cfg->spi_bus; /* set parity */ tx |= !spi_parity_odd(tx); /* Acquire ownership of the bus. */ spiAcquireBus(spi); /* Setup transfer parameters. */ spiStart(spi, &cfg->spi_config); /* Slave Select assertion. */ spiSelect(spi); /* Atomic transfer operations. */ rx = spiPolledExchange(spi, tx); /* Slave Select de-assertion. */ spiUnselect(spi); /* Ownership release. */ spiReleaseBus(spi); /* statisctic and debug */ this->tx = tx; this->rx = rx; this->spi_cnt++; if (rx_ptr) *rx_ptr = rx; /* validate reply */ ret = spi_validate(rx); /* save last accessed register */ last_addr = MSG_GET_ADDR(this->tx); if (last_addr == MSG_READ_ADDR) last_subaddr = MSG_GET_SUBADDR(this->tx); else last_subaddr = REG_INVALID; return ret; } /** * @return -1 in case of communication error */ int L9779::spi_rw_array(const uint16_t *tx, uint16_t *rx, int n) { int ret = 0; SPIDriver *spi = cfg->spi_bus; if (n <= 0) { return -2; } /* Acquire ownership of the bus. */ spiAcquireBus(spi); /* Setup transfer parameters. */ spiStart(spi, &cfg->spi_config); for (int i = 0; i < n; i++) { /* Slave Select assertion. */ spiSelect(spi); /* data transfer */ uint16_t rxdata = spiPolledExchange(spi, tx[i]); if (rx) rx[i] = rxdata; /* Slave Select de-assertion. */ spiUnselect(spi); /* statistic and debug */ this->tx = tx[i]; this->rx = rxdata; this->spi_cnt++; /* validate reply */ ret = spi_validate(rxdata); /* save last accessed register */ last_addr = MSG_GET_ADDR(this->tx); if (last_addr == MSG_READ_ADDR) last_subaddr = MSG_GET_SUBADDR(this->tx); else last_subaddr = REG_INVALID; if (ret < 0) break; } /* Ownership release. */ spiReleaseBus(spi); /* no errors for now */ return ret; } /* use datasheet numbering, starting from 1, skip 4 ignition channels */ #define OUT_ENABLED(n) (!!(o_state & BIT((n) + L9779_OUTPUTS_IGN - 1))) #define SHIFT_N_OUT_TO_M(n, m) (OUT_ENABLED(n) << (m)) /* use datasheet numbering, starting from 1 */ #define IGN_ENABLED(n) (!!(o_state & BIT((n) - 1))) #define SHIFT_N_IGN_TO_M(n, m) (IGN_ENABLED(n) << (m)) int L9779::update_output() { int ret; uint8_t regs[4]; /* set value only for non-direct driven pins */ uint32_t o_data = o_state & ~OUT_DIRECT_DRIVE_MASK; /* direct driven outputs are logicaly-AND spi bit and dedicated input * set bits to all enabled direct driven outputs */ o_data = o_state | (o_oe_mask & OUT_DIRECT_DRIVE_MASK); /* nightmare... briliant mapping */ regs[0] = SHIFT_N_OUT_TO_M( 1, 7) | /* bit 7 - OUT1 */ SHIFT_N_OUT_TO_M( 2, 6) | /* and so on, refer to datasheet */ SHIFT_N_OUT_TO_M( 3, 5) | SHIFT_N_OUT_TO_M( 4, 4) | SHIFT_N_OUT_TO_M( 5, 3) | SHIFT_N_OUT_TO_M(20, 2); regs[1] = SHIFT_N_OUT_TO_M(15, 7) | SHIFT_N_OUT_TO_M(14, 6) | /* reserved + don't care */ SHIFT_N_IGN_TO_M( 1, 3) | SHIFT_N_IGN_TO_M( 2, 2) | SHIFT_N_IGN_TO_M( 3, 1) | SHIFT_N_IGN_TO_M( 4, 0); regs[2] = SHIFT_N_OUT_TO_M(22, 7) | /* TODO: stepper DIR */ SHIFT_N_OUT_TO_M(21, 6) | /* TODO: stepper enable */ SHIFT_N_OUT_TO_M(16, 5) | SHIFT_N_OUT_TO_M(14, 4) | SHIFT_N_OUT_TO_M(17, 3) | SHIFT_N_OUT_TO_M(18, 2) | SHIFT_N_OUT_TO_M( 7, 1) | SHIFT_N_OUT_TO_M( 6, 0); regs[3] = SHIFT_N_OUT_TO_M(28, 5) | SHIFT_N_OUT_TO_M(27, 4) | SHIFT_N_OUT_TO_M(26, 3) | SHIFT_N_OUT_TO_M(25, 2) | SHIFT_N_OUT_TO_M(24, 1) | SHIFT_N_OUT_TO_M(23, 0); /* TODO: stepper PWM */ uint16_t tx[] = { /* output enables */ CMD_CONTR_REG(0, regs[0]), CMD_CONTR_REG(1, regs[1]), CMD_CONTR_REG(2, regs[2]), CMD_CONTR_REG(3, regs[3]) }; ret = spi_rw_array(tx, NULL, ARRAY_SIZE(tx)); if (ret == 0) { /* atomic */ o_data_cached = o_data; } return ret; } int L9779::update_direct_output(size_t pin, int value) { /* no direct-drive gpio is allocated for this output */ if (cfg->direct_gpio[pin].port == NULL) return -1; if (value) palSetPort(cfg->direct_gpio[pin].port, PAL_PORT_BIT(cfg->direct_gpio[pin].pad)); else palClearPort(cfg->direct_gpio[pin].port, PAL_PORT_BIT(cfg->direct_gpio[pin].pad)); return 0; } /** * @brief L9779 chip driver wakeup. * @details Wake up driver. Will cause output register update */ int L9779::wake_driver() { /* Entering a reentrant critical zone.*/ chibios_rt::CriticalSectionLocker csl; chSemSignalI(&wake); if (!port_is_isr_context()) { /** * chSemSignalI above requires rescheduling * interrupt handlers have implicit rescheduling */ chSchRescheduleS(); } return 0; } int L9779::chip_reset() { int ret; ret = spi_rw(CMD_CLOCK_UNLOCK_SW_RST(BIT(1)), NULL); /** * ??? */ chThdSleepMilliseconds(3); last_addr = REG_INVALID; last_subaddr = REG_INVALID; return ret; } /*==========================================================================*/ /* Driver thread. */ /*==========================================================================*/ static THD_FUNCTION(l9779_driver_thread, p) { L9779 *chip = reinterpret_cast(p); sysinterval_t poll_interval = 0; chRegSetThreadName(DRIVER_NAME); while (1) { int ret; msg_t msg = chSemWaitTimeout(&chip->wake, poll_interval); /* should we care about msg == MSG_TIMEOUT? */ (void)msg; /* default polling interval */ poll_interval = TIME_MS2I(DIAG_PERIOD_MS); if ((chip->cfg == NULL) || (chip->drv_state == L9779_DISABLED) || (chip->drv_state == L9779_FAILED)) continue; #if 0 bool wd_happy = chip->wd_happy; /* update outputs only if WD is happy */ if ((wd_happy) || (1)) { ret = chip->update_output(); if (ret) { /* set state to L9779_FAILED? */ } } ret = chip->wd_feed(); if (ret < 0) { /* WD is not happy */ continue; } /* happiness state has changed! */ if ((chip->wd_happy != wd_happy) && (chip->wd_happy)) { chip->need_init = true; } #endif if (chip->need_init) { /* clear first, as flag can be raised again during init */ chip->need_init = false; /* re-init chip! */ chip->chip_init(); /* sync pins state */ chip->update_output(); } /* Chip is ready to rock? */ if (chip->need_init == false) { /* Just update outputs state */ ret = chip->update_output(); if (ret) { /* set state to L9779_FAILED? */ } } #if 0 if (chip->diag_ts <= chVTGetSystemTimeX()) { /* this is expensive call, will do a lot of spi transfers... */ ret = chip->update_status_and_diag(); if (ret) { /* set state to L9779_FAILED or force reinit? */ } else { diagResponse.reset(); } /* TODO: * Procedure to switch on after failure condition occurred: * - Read out of diagnosis bits * - Second read out to verify that the failure conditions are not * remaining * - Set of the dedicated output enable bit of the affected channel * if the diagnosis bit is not active anymore * - Switch on of the channel */ chip->diag_ts = chTimeAddX(chVTGetSystemTimeX(), TIME_MS2I(DIAG_PERIOD_MS)); } poll_interval = chip->calc_sleep_interval(); #endif /* default poll_interval */ } } /*==========================================================================*/ /* Driver interrupt handlers. */ /*==========================================================================*/ /*==========================================================================*/ /* Driver exported functions. */ /*==========================================================================*/ int L9779::setPadMode(unsigned int pin, iomode_t mode) { if (pin >= L9779_SIGNALS) return -1; (void)mode; return 0; } int L9779::writePad(unsigned int pin, int value) { if (pin >= L9779_OUTPUTS) return -1; { chibios_rt::CriticalSectionLocker csl; if (value) { o_state |= (1 << pin); } else { o_state &= ~(1 << pin); } } /* direct driven? */ if (OUT_DIRECT_DRIVE_MASK & BIT(pin)) { return update_direct_output(pin, value); } else { return wake_driver(); } return 0; } brain_pin_diag_e L9779::getOutputDiag(size_t pin) { (void)pin; return PIN_OK; } brain_pin_diag_e L9779::getInputDiag(unsigned int pin) { (void)pin; return PIN_OK; } int L9779::readPad(size_t pin) { if (pin >= L9779_SIGNALS) return -1; /* unknown pin */ return -1; } brain_pin_diag_e L9779::getDiag(size_t pin) { if (pin >= L9779_SIGNALS) return PIN_INVALID; if (pin < L9779_OUTPUTS) return getOutputDiag(pin); else return getInputDiag(pin); } int L9779::chip_init_data(void) { int i; int ret = 0; o_oe_mask = 0; for (i = 0; i < L9779_DIRECT_OUTPUTS; i++) { if (cfg->direct_gpio[i].port == NULL) continue; /* configure source gpio */ ret = gpio_pin_markUsed(cfg->direct_gpio[i].port, cfg->direct_gpio[i].pad, DRIVER_NAME " DIRECT IO"); if (ret) { ret = -1; goto err_gpios; } palSetPadMode(cfg->direct_gpio[i].port, cfg->direct_gpio[i].pad, PAL_MODE_OUTPUT_PUSHPULL); palClearPort(cfg->direct_gpio[i].port, PAL_PORT_BIT(cfg->direct_gpio[i].pad)); /* enable output */ o_oe_mask |= BIT(i); } /* enable all spi-driven ouputs * TODO: add API to enable/disable? */ o_oe_mask |= ~OUT_DIRECT_DRIVE_MASK; return 0; err_gpios: /* unmark pins */ for (int i = 0; i < L9779_DIRECT_OUTPUTS; i++) { if (cfg->direct_gpio[i].port) { gpio_pin_markUnused(cfg->direct_gpio[i].port, cfg->direct_gpio[i].pad); } } return ret; } int L9779::chip_init() { int ret; /* statistic */ init_cnt++; /* Unlock, while unlocked by default. */ ret = spi_rw(CMD_CLOCK_UNLOCK_SW_RST(0), NULL); if (ret) return ret; /* Enable power stages */ ret = spi_rw(CMD_START_REACT(BIT(1)), NULL); if (ret) return ret; /* TODO: add spi communication test: read IDENT_REG */ return ret; } int L9779::init() { int ret; /* check for multiple init */ if (drv_state != L9779_WAIT_INIT) return -1; ret = chip_reset(); if (ret) return ret; ret = chip_init_data(); if (ret) return ret; /* force chip init from driver thread */ need_init = true; /* instance is ready */ drv_state = L9779_READY; /* init semaphore */ chSemObjectInit(&wake, 10); /* start thread */ thread = chThdCreateStatic(thread_wa, sizeof(thread_wa), PRIO_GPIOCHIP, l9779_driver_thread, this); return 0; } int L9779::deinit() { return 0; } /** * @brief L9779 driver add. * @details Checks for valid config * @return return gpio chip base */ int l9779_add(brain_pin_e base, unsigned int index, const l9779_config *cfg) { efiAssert(OBD_PCM_Processor_Fault, cfg != NULL, "L9779CFG", 0) /* no config or no such chip */ if ((!cfg) || (!cfg->spi_bus) || (index >= BOARD_L9779_COUNT)) return -1; L9779* chip = &chips[index]; /* already initted? */ if (chip->cfg) return -1; /* config */ chip->cfg = cfg; /* reset to defaults */ /* register */ int ret = gpiochip_register(base, DRIVER_NAME, *chip, L9779_SIGNALS); if (ret < 0) return ret; /* set default pin names, board init code can rewrite */ gpiochips_setPinNames(base, l9779_pin_names); return ret; } #endif /* (BOARD_L9779_COUNT > 0) */