/** library to drive vacuum fluorescent display using supertex HV518 shift register VFD drivers * @details the current configuration is for a VFD extracted from a Samsung SER-6500 cash register * @file * @author King Kévin * @copyright SPDX-License-Identifier: GPL-3.0-or-later * @date 2016-2020 * @note peripherals used: SPI @ref vfd_hv518_spi , GPIO @ref vfd_hv518_gpio , timer @ref vfd_hv518_timer */ /* standard libraries */ #include // standard integer types #include // general utilities /* STM32 (including CM3) libraries */ #include // real-time control clock library #include // general purpose input output library #include // SPI library #include // timer library #include // interrupt handler #include "global.h" // global definitions #include "vfd_hv518.h" // VFD library API /** @defgroup vfd_hv518_gpio GPIO to control supertex HV518 VFD drivers * @{ */ #define VFD_STR_PIN PA6 /**< strobe pin to enable high voltage output, high voltage is output on low */ #define VFD_NLE_PIN PA4 /**< latch enable pin, stores the shifted data on low, output the parallel data on high */ /** @} */ /** @defgroup vfd_hv518_spi SPI to send data to supertex HV518 VFD drivers * @{ */ #define VFD_SPI 1 /**< SPI peripheral */ /** @} */ /** @defgroup vfd_hv518_timer timer for automatic display blocks refresh * @{ */ #define VFD_TIMER 2 /**< timer peripheral ID */ /** @} */ /** ASCII characters encoded for the 7 segments digit block * @note starts with space */ static const uint8_t ascii_7segments[] = { 0x00, // 0b00000000, space 0x30, // 0b00110000, ! (I) 0x22, // 0b00100010, " 0x5c, // 0b01011100, # (o) 0x6d, // 0b01101101, $ (s) 0x52, // 0b01010010, % (/) 0x7d, // 0b01111101, & (6) 0x20, // 0b00100000, ' 0x39, // 0b00111001, ( ([) 0x0f, // 0b00001111, ) 0x70, // 0b01110000, * 0x46, // 0b01000110, + 0x10, // 0b00010000, , 0x40, // 0b01000000, - 0x10, // 0b00010000, . (,) 0x52, // 0b01010010, / 0x3f, // 0b00111111, 0 0x06, // 0b00000110, 1 0x5b, // 0b01011011, 2 0x4f, // 0b01001111, 3 0x66, // 0b01100110, 4 0x6d, // 0b01101101, 5 0x7d, // 0b01111101, 6 0x07, // 0b00000111, 7 0x7f, // 0b01111111, 8 0x6f, // 0b01101111, 9 0x48, // 0b01001000, : (=) 0x48, // 0b01001000, ; (=) 0x58, // 0b01011000, < 0x48, // 0b01001000, = 0x4c, // 0b01001100, > 0x53, // 0b01010011, ? 0x7b, // 0b01111011, @ 0x77, // 0b01110111, A 0x7f, // 0b01111111, B 0x39, // 0b00111001, C 0x5e, // 0b01011110, D 0x79, // 0b01111001, E 0x71, // 0b01110001, F 0x3d, // 0b00111101, G 0x76, // 0b01110110, H 0x30, // 0b00110000, I 0x1e, // 0b00011110, J 0x76, // 0b01110110, K 0x38, // 0b00111000, L 0x37, // 0b00110111, M 0x37, // 0b00110111, N 0x3f, // 0b00111111, O 0x73, // 0b01110011, P 0x6b, // 0b01101011, Q 0x33, // 0b00110011, R 0x6d, // 0b01101101, S 0x78, // 0b01111000, T 0x3e, // 0b00111110, U 0x3e, // 0b00111110, V (U) 0x3e, // 0b00111110, W (U) 0x76, // 0b01110110, X (H) 0x6e, // 0b01101110, Y 0x5b, // 0b01011011, Z 0x39, // 0b00111001, [ 0x64, // 0b01100100, '\' 0x0f, // 0b00001111, / 0x23, // 0b00100011, ^ 0x08, // 0b00001000, _ 0x02, // 0b00000010, ` 0x5f, // 0b01011111, a 0x7c, // 0b01111100, b 0x58, // 0b01011000, c 0x5e, // 0b01011110, d 0x7b, // 0b01111011, e 0x71, // 0b01110001, f 0x6f, // 0b01101111, g 0x74, // 0b01110100, h 0x10, // 0b00010000, i 0x0c, // 0b00001100, j 0x76, // 0b01110110, k 0x30, // 0b00110000, l 0x54, // 0b01010100, m 0x54, // 0b01010100, n 0x5c, // 0b01011100, o 0x73, // 0b01110011, p 0x67, // 0b01100111, q 0x50, // 0b01010000, r 0x6d, // 0b01101101, s 0x78, // 0b01111000, t 0x1c, // 0b00011100, u 0x1c, // 0b00011100, v (u) 0x1c, // 0b00011100, w (u) 0x76, // 0b01110110, x 0x6e, // 0b01101110, y 0x5b, // 0b01011011, z 0x39, // 0b00111001, { ([) 0x30, // 0b00110000, | 0x0f, // 0b00001111, } ([) 0x40, // 0b01000000, ~ }; /** font for the 5x7 dot matrix block * @details first value is left-most line, LSB is top dot, MSB is not used * @note from http://sunge.awardspace.com/glcd-sd/node4.html */ static const uint8_t font5x7[][5] = { {0x00, 0x00, 0x00, 0x00, 0x00}, // (space) {0x00, 0x00, 0x5F, 0x00, 0x00}, // ! {0x00, 0x07, 0x00, 0x07, 0x00}, // " {0x14, 0x7F, 0x14, 0x7F, 0x14}, // # {0x24, 0x2A, 0x7F, 0x2A, 0x12}, // $ {0x23, 0x13, 0x08, 0x64, 0x62}, // % {0x36, 0x49, 0x55, 0x22, 0x50}, // & {0x00, 0x05, 0x03, 0x00, 0x00}, // ' {0x00, 0x1C, 0x22, 0x41, 0x00}, // ( {0x00, 0x41, 0x22, 0x1C, 0x00}, // ) {0x08, 0x2A, 0x1C, 0x2A, 0x08}, // * {0x08, 0x08, 0x3E, 0x08, 0x08}, // + {0x00, 0x50, 0x30, 0x00, 0x00}, // , {0x08, 0x08, 0x08, 0x08, 0x08}, // - {0x00, 0x60, 0x60, 0x00, 0x00}, // . {0x20, 0x10, 0x08, 0x04, 0x02}, // / {0x3E, 0x51, 0x49, 0x45, 0x3E}, // 0 {0x00, 0x42, 0x7F, 0x40, 0x00}, // 1 {0x42, 0x61, 0x51, 0x49, 0x46}, // 2 {0x21, 0x41, 0x45, 0x4B, 0x31}, // 3 {0x18, 0x14, 0x12, 0x7F, 0x10}, // 4 {0x27, 0x45, 0x45, 0x45, 0x39}, // 5 {0x3C, 0x4A, 0x49, 0x49, 0x30}, // 6 {0x01, 0x71, 0x09, 0x05, 0x03}, // 7 {0x36, 0x49, 0x49, 0x49, 0x36}, // 8 {0x06, 0x49, 0x49, 0x29, 0x1E}, // 9 {0x00, 0x36, 0x36, 0x00, 0x00}, // : {0x00, 0x56, 0x36, 0x00, 0x00}, // ; {0x00, 0x08, 0x14, 0x22, 0x41}, // < {0x14, 0x14, 0x14, 0x14, 0x14}, // = {0x41, 0x22, 0x14, 0x08, 0x00}, // > {0x02, 0x01, 0x51, 0x09, 0x06}, // ? {0x32, 0x49, 0x79, 0x41, 0x3E}, // @ {0x7E, 0x11, 0x11, 0x11, 0x7E}, // A {0x7F, 0x49, 0x49, 0x49, 0x36}, // B {0x3E, 0x41, 0x41, 0x41, 0x22}, // C {0x7F, 0x41, 0x41, 0x22, 0x1C}, // D {0x7F, 0x49, 0x49, 0x49, 0x41}, // E {0x7F, 0x09, 0x09, 0x01, 0x01}, // F {0x3E, 0x41, 0x41, 0x51, 0x32}, // G {0x7F, 0x08, 0x08, 0x08, 0x7F}, // H {0x00, 0x41, 0x7F, 0x41, 0x00}, // I {0x20, 0x40, 0x41, 0x3F, 0x01}, // J {0x7F, 0x08, 0x14, 0x22, 0x41}, // K {0x7F, 0x40, 0x40, 0x40, 0x40}, // L {0x7F, 0x02, 0x04, 0x02, 0x7F}, // M {0x7F, 0x04, 0x08, 0x10, 0x7F}, // N {0x3E, 0x41, 0x41, 0x41, 0x3E}, // O {0x7F, 0x09, 0x09, 0x09, 0x06}, // P {0x3E, 0x41, 0x51, 0x21, 0x5E}, // Q {0x7F, 0x09, 0x19, 0x29, 0x46}, // R {0x46, 0x49, 0x49, 0x49, 0x31}, // S {0x01, 0x01, 0x7F, 0x01, 0x01}, // T {0x3F, 0x40, 0x40, 0x40, 0x3F}, // U {0x1F, 0x20, 0x40, 0x20, 0x1F}, // V {0x7F, 0x20, 0x18, 0x20, 0x7F}, // W {0x63, 0x14, 0x08, 0x14, 0x63}, // X {0x03, 0x04, 0x78, 0x04, 0x03}, // Y {0x61, 0x51, 0x49, 0x45, 0x43}, // Z {0x00, 0x00, 0x7F, 0x41, 0x41}, // [ {0x02, 0x04, 0x08, 0x10, 0x20}, // '\' {0x41, 0x41, 0x7F, 0x00, 0x00}, // ] {0x04, 0x02, 0x01, 0x02, 0x04}, // ^ {0x40, 0x40, 0x40, 0x40, 0x40}, // _ {0x00, 0x01, 0x02, 0x04, 0x00}, // ` {0x20, 0x54, 0x54, 0x54, 0x78}, // a {0x7F, 0x48, 0x44, 0x44, 0x38}, // b {0x38, 0x44, 0x44, 0x44, 0x20}, // c {0x38, 0x44, 0x44, 0x48, 0x7F}, // d {0x38, 0x54, 0x54, 0x54, 0x18}, // e {0x08, 0x7E, 0x09, 0x01, 0x02}, // f {0x08, 0x14, 0x54, 0x54, 0x3C}, // g {0x7F, 0x08, 0x04, 0x04, 0x78}, // h {0x00, 0x44, 0x7D, 0x40, 0x00}, // i {0x20, 0x40, 0x44, 0x3D, 0x00}, // j {0x00, 0x7F, 0x10, 0x28, 0x44}, // k {0x00, 0x41, 0x7F, 0x40, 0x00}, // l {0x7C, 0x04, 0x18, 0x04, 0x78}, // m {0x7C, 0x08, 0x04, 0x04, 0x78}, // n {0x38, 0x44, 0x44, 0x44, 0x38}, // o {0x7C, 0x14, 0x14, 0x14, 0x08}, // p {0x08, 0x14, 0x14, 0x18, 0x7C}, // q {0x7C, 0x08, 0x04, 0x04, 0x08}, // r {0x48, 0x54, 0x54, 0x54, 0x20}, // s {0x04, 0x3F, 0x44, 0x40, 0x20}, // t {0x3C, 0x40, 0x40, 0x20, 0x7C}, // u {0x1C, 0x20, 0x40, 0x20, 0x1C}, // v {0x3C, 0x40, 0x30, 0x40, 0x3C}, // w {0x44, 0x28, 0x10, 0x28, 0x44}, // x {0x0C, 0x50, 0x50, 0x50, 0x3C}, // y {0x44, 0x64, 0x54, 0x4C, 0x44}, // z {0x00, 0x08, 0x36, 0x41, 0x00}, // { {0x00, 0x00, 0x7F, 0x00, 0x00}, // | {0x00, 0x41, 0x36, 0x08, 0x00}, // } {0x08, 0x04, 0x0c, 0x08, 0x04}, // ~ {0b00001000, 0b00000100, 0b00001100, 0b00001000, 0b00000100} }; /** pictures for the 5x7 dot matrix block * @details first value is left-most line, LSB is top dot, MSB is not used */ static const uint8_t pict5x7[][5] = { {0x08, 0x08, 0x2A, 0x1C, 0x08}, // -> {0x08, 0x1C, 0x2A, 0x08, 0x08}, // <- {0x70, 0x70, 0x7a, 0x7c, 0x58}, // bunny side 1 {0b01110000, 0b01110000, 0b01111010, 0b01111100, 0b01011000} {0x20, 0x70, 0x72, 0x7c, 0x58}, // bunny side 2 {0b00100000, 0b01110000, 0b01110010, 0b01111100, 0b01011000} {0x3e, 0x49, 0x56, 0x49, 0x3e}, // bunny face 1 {0b00111110, 0b01001001, 0b01010110, 0b01001001, 0b00111110} {0x3e, 0x51, 0x66, 0x51, 0x3e}, // bunny face 2 {0b00111110, 0b01010001, 0b01100110, 0b01010001, 0b00111110} {0x38, 0x57, 0x64, 0x57, 0x38}, // bunny face 3 {0b00111000, 0b01010111, 0b01100100, 0b01010111, 0b00111000} {0x38, 0x4f, 0x54, 0x4f, 0x38}, // bunny face 4 {0b00111000, 0b01001111, 0b01010100, 0b01001111, 0b00111000} {0x38, 0x5e, 0x68, 0x5e, 0x38}, // bunny face 5 {0b00111000, 0b01011110, 0b01101000, 0b01011110, 0b00111000} {0x41, 0x36, 0x08, 0x36, 0x41}, // cross 1 {0b01000001, 0b00110110, 0b00001000, 0b00110110, 0b01000001} {~0x41, ~0x36, ~0x08, ~0x36, ~0x41}, // cross 1 negated {~0b01000001, ~0b00110110, ~0b00001000, ~0b00110110, ~0b01000001} {0x22, 0x14, 0x08, 0x14, 0x22}, // cross 2 {0b00100010, 0b00010100, 0b00001000, 0b00010100, 0b00100010} {~0x22, ~0x14, ~0x08, ~0x14, ~0x22}, // cross 2 negated {~0b00100010, ~0b00010100, ~0b00001000, ~0b00010100, ~0b00100010} {0x00, 0x00, 0x00, 0x00, 0x00}, // nothing }; /** the 32 bits values to be shifted out to the VFD driver * @note split into 16 bit for SPI transfer * @note since the bits for digits and matrix are independent, they can be combined * @note we have more matrix (12) than digits (10) */ static uint16_t driver_data[VFD_MATRIX][VFD_DRIVERS * 2] = {0}; /** which driver data is being transmitted */ static volatile uint8_t spi_i = 0; /** which grid/part to activate * @note digits and matrix can be combined */ static volatile uint8_t vfd_grid = 0; /** the bits used for selecting then digit and 7 segment anodes * @note for the second driver */ static const uint32_t digit_mask = 0x00fffff0; void vfd_digit(uint8_t nb, char c) { if (!(nb < VFD_DIGITS)) { // check the digit exists return; } uint32_t digit_data = 0; // the data to be shifted out for the driver (for the second driver) digit_data = 1 << (4 + (9 - nb)); // select digit /* encode segment * here the bit order (classic 7 segment + underline and dot) * 3_ * 8|9_|4 * 7|6_|5.1 * 0_2, * */ if (false) { // add the underline (not encoded) digit_data |= (1 << 14); } if (c&0x80) { // add the dot (encoded in the 8th bit) digit_data |= (1 << 15); } if (false) { // add the comma (not encoded) digit_data |= (1 << 16); } c &= 0x7f; // only take the ASCII part if (c >= ' ') { // only take printable characters uint8_t i = c - ' '; // get index for character if (i < LENGTH(ascii_7segments)) { digit_data |= (ascii_7segments[i] << 17); // add encoded segments to memory } } digit_data &= digit_mask; // be sure only the bits for the digit are used digit_data |= (driver_data[nb][2] + (driver_data[nb][3] << 16)) & ~digit_mask; // get the existing data and add the bits for the digit driver_data[nb][2] = digit_data; // write back data (least significant half) driver_data[nb][3] = (digit_data >> 16); // write back data (most significant half) } void vfd_matrix(uint8_t nb, char c) { // check the matrix exists if (!(nb < VFD_MATRIX)) { return; } uint32_t matrix_data[VFD_DRIVERS] = {0}; // the data to be shifted out for the driver // select matrix if (nb < 4) { matrix_data[1] = 1 << (3 - nb); } else { matrix_data[0] = 1 << (35 - nb); } if ((c < 0x80) && (c >= ' ')) { // only take printable characters uint8_t i = c - ' '; // get index for character if (i < LENGTH(font5x7)) { matrix_data[1] |= font5x7[i][0] << 24; matrix_data[2] |= font5x7[i][1] << 0; matrix_data[2] |= font5x7[i][2] << 8; matrix_data[2] |= font5x7[i][3] << 16; matrix_data[2] |= font5x7[i][4] << 24; } } else if (c > 0x7f) { // the non ASCII character are used for pictures uint8_t i = c - 0x80; // get index for character if (i < LENGTH(pict5x7)) { matrix_data[1] |= pict5x7[i][0] << 24; matrix_data[2] |= pict5x7[i][1] << 0; matrix_data[2] |= pict5x7[i][2] << 8; matrix_data[2] |= pict5x7[i][3] << 16; matrix_data[2] |= pict5x7[i][4] << 24; } } matrix_data[1] &= ~digit_mask; // be sure only the bits for the matrix are used matrix_data[1] |= (driver_data[nb][2] + (driver_data[nb][3] << 16)) & digit_mask; // get the existing data for the digit // prepare the data for SPI to shift it out for (uint8_t i = 0; i < LENGTH(matrix_data); i++) { driver_data[nb][i * 2] = matrix_data[i]; driver_data[nb][i * 2 + 1] = matrix_data[i] >> 16; } } void vfd_clear(void) { for (uint8_t i = 0; i < LENGTH(driver_data); i++) { for (uint8_t j = 0; j < LENGTH(driver_data[0]); j++) { driver_data[i][j] = 0; } } } void vfd_test(void) { for (uint8_t i = 0; i < LENGTH(driver_data); i++) { for (uint8_t j = 0; j < LENGTH(driver_data[0]); j++) { driver_data[i][j] = ~0; } } } void vfd_on(void) { gpio_clear(GPIO_PORT(VFD_STR_PIN), GPIO_PIN(VFD_STR_PIN)); // enable HV output timer_enable_counter(TIM(VFD_TIMER)); // start timer to periodically output that to the parts } void vfd_off(void) { gpio_set(GPIO_PORT(VFD_STR_PIN), GPIO_PIN(VFD_STR_PIN)); // disable HV output timer_disable_counter(TIM(VFD_TIMER)); // stop timer to periodically output that to the parts } void vfd_setup(void) { /* setup GPIO to control the VFD */ rcc_periph_clock_enable(GPIO_RCC(VFD_STR_PIN)); // enable clock for VFD GPIO gpio_set(GPIO_PORT(VFD_STR_PIN), GPIO_PIN(VFD_STR_PIN)); // disable HV output gpio_set_mode(GPIO_PORT(VFD_STR_PIN), GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO_PIN(VFD_STR_PIN)); // set VFD pin to output push-pull rcc_periph_clock_enable(GPIO_RCC(VFD_NLE_PIN)); // enable clock for VFD GPIO gpio_clear(GPIO_PORT(VFD_NLE_PIN), GPIO_PIN(VFD_NLE_PIN)); // do not output latched data gpio_set_mode(GPIO_PORT(VFD_NLE_PIN), GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO_PIN(VFD_NLE_PIN)); // set VFD pin to output push-pull /* setup SPI to transmit data */ rcc_periph_clock_enable(RCC_SPI(VFD_SPI)); // enable SPI clock rcc_periph_clock_enable(RCC_SPI_SCK_PORT(VFD_SPI)); // enable clock for VFD SPI GPIO rcc_periph_clock_enable(RCC_SPI_MOSI_PORT(VFD_SPI)); // enable clock for VFD SPI GPIO gpio_set_mode(SPI_SCK_PORT(VFD_SPI), GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, SPI_SCK_PIN(VFD_SPI)); // set VFD pin to alternative function push-pull gpio_set_mode(SPI_MOSI_PORT(VFD_SPI), GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, SPI_MOSI_PIN(VFD_SPI)); // set VFD pin to alternative function push-pull spi_reset(SPI(VFD_SPI)); // clear SPI values /* set SPI: * - use VFD_SPI port * - divide clock by 8 for generating the baudrate (F_PCLK1 is 36MHz, max HV518 is 6MHz) * - clock idle high polarity * - data is valid on rising edge (second clock phase) * - send 16 bits at a time * - send least significant bit first (that's how I coded the data) */ spi_init_master(SPI(VFD_SPI), SPI_CR1_BAUDRATE_FPCLK_DIV_8, SPI_CR1_CPOL_CLK_TO_1_WHEN_IDLE, SPI_CR1_CPHA_CLK_TRANSITION_2, SPI_CR1_DFF_16BIT, SPI_CR1_LSBFIRST); //spi_set_bidirectional_transmit_only_mode(VFD_SPI); // only use MOSI to transmit spi_set_unidirectional_mode(SPI(VFD_SPI)); // MISO is unused /* set NSS high to enable transmission * the NSS in STM32 can not be used as hardware slave select * RM0008 reference manual 25.3.1 is misleading * when hardware NSS is used and output is enabled NSS never goes up after transmission, even if SPI is disabled * when software NSS is used, NSS can not be set high again, even when writing to the register * the slave select must be done manually using GPIO */ spi_enable_software_slave_management(SPI(VFD_SPI)); spi_set_nss_high(SPI(VFD_SPI)); // set NSS high nvic_enable_irq(SPI_IRQ(VFD_SPI)); // enable SPI interrupt spi_enable(SPI(VFD_SPI)); // enable SPI (the tx empty interrupt will trigger) /* setup timer to refresh display */ rcc_periph_clock_enable(RCC_TIM(VFD_TIMER)); // enable clock for timer block rcc_periph_reset_pulse(RST_TIM(VFD_TIMER)); // reset timer state timer_set_mode(TIM(VFD_TIMER), TIM_CR1_CKD_CK_INT, TIM_CR1_CMS_EDGE, TIM_CR1_DIR_UP); // set timer mode, use undivided timer clock,edge alignment (simple count), and count up timer_set_prescaler(TIM(VFD_TIMER), (rcc_ahb_frequency / (1 << 16)) - 1); // set the prescaler so this 16 bits timer overflows at 1Hz timer_set_period(TIM(VFD_TIMER), 0xffff / LENGTH(driver_data) / 100); // set the refresh frequency timer_enable_irq(TIM(VFD_TIMER), TIM_DIER_UIE); // enable interrupt for timer nvic_enable_irq(NVIC_TIM_IRQ(VFD_TIMER)); // allow interrupt for timer vfd_clear(); // initialize values } /** SPI interrupt service routine called when data has been transmitted */ void SPI_ISR(VFD_SPI)(void) { if (SPI_SR(SPI(VFD_SPI)) & SPI_SR_TXE) { // transmission buffer empty if (spi_i < LENGTH(driver_data[0])) { // check if data is available gpio_clear(GPIO_PORT(VFD_NLE_PIN), GPIO_PIN(VFD_NLE_PIN)); // slave select to latch data spi_send(SPI(VFD_SPI), driver_data[vfd_grid][spi_i++]); // send next data } else { // all data transmitted spi_disable_tx_buffer_empty_interrupt(SPI(VFD_SPI)); // no need to wait for new data while (SPI_SR(SPI(VFD_SPI)) & SPI_SR_BSY); // wait for data to be shifted out spi_disable_tx_buffer_empty_interrupt(SPI(VFD_SPI)); // no need to wait for new data gpio_set(GPIO_PORT(VFD_NLE_PIN), GPIO_PIN(VFD_NLE_PIN)); // output latched data } } } /** timer interrupt service routine called time passed */ void TIM_ISR(VFD_TIMER)(void) { if (timer_get_flag(TIM(VFD_TIMER), TIM_SR_UIF)) { // overflow even happened timer_clear_flag(TIM(VFD_TIMER), TIM_SR_UIF); // clear flag spi_i = 0; // set the register to shift out spi_enable_tx_buffer_empty_interrupt(SPI(VFD_SPI)); // enable TX empty interrupt vfd_grid = (vfd_grid + 1) % LENGTH(driver_data); // got to next segment } }