// display.c (HT16K33 14-seg / 4 dígitos) // TOP = 0x71 | BOTTOM = 0x70 // I2C_PORT = I2C_NUM_0 // // MODO "SAFE": // - Se I2C não estiver instalado, ou se não existir display, ignora tudo (sem spam) // - display_init() NUNCA aborta o firmware (devolve ESP_OK e deixa display disabled) #include "driver/i2c.h" #include "esp_log.h" #include "esp_err.h" #include "display.h" #include #include #define I2C_PORT I2C_NUM_0 // Endereços reais #define DISP_TOP_ADDR 0x71 #define DISP_BOTTOM_ADDR 0x70 static const char *TAG = "DISPLAY"; // ======================================================= // FLAG GLOBAL // ======================================================= static bool s_display_enabled = false; void display_set_enabled(bool en) { s_display_enabled = en; } bool display_is_enabled(void) { return s_display_enabled; } // ======================================================= // MAPA DE SEGMENTOS (igual ao teu) // ======================================================= #define SEG_A (1 << 0) #define SEG_B (1 << 1) #define SEG_C (1 << 2) #define SEG_D (1 << 3) #define SEG_E (1 << 4) #define SEG_F (1 << 5) #define SEG_ML (1 << 6) #define SEG_MR (1 << 7) #define SEG_TL (1 << 8) #define SEG_TM (1 << 9) #define SEG_TR (1 << 10) #define SEG_BL (1 << 11) #define SEG_BM (1 << 12) #define SEG_BR (1 << 13) #define SEG_DP (1 << 14) #define SEG_G (SEG_ML | SEG_MR) static uint16_t charset(char c); // ======================================================= // FUNÇÕES I2C (internas) // ======================================================= static inline bool i2c_driver_is_installed(void) { // truque simples: tenta criar um cmd e dar begin "vazio" é overkill. // Mais seguro: tenta uma escrita curta e ver se dá INVALID_STATE. // Aqui vamos só confiar na primeira escrita do display_init(). return true; } static esp_err_t disp_send_cmd(uint8_t addr, uint8_t cmd) { return i2c_master_write_to_device(I2C_PORT, addr, &cmd, 1, pdMS_TO_TICKS(50)); } static esp_err_t disp_raw(uint8_t addr, int pos, uint16_t mask) { if (pos < 0 || pos > 3) return ESP_ERR_INVALID_ARG; uint8_t buf[3]; buf[0] = (uint8_t)(pos * 2); buf[1] = (uint8_t)(mask & 0xFF); buf[2] = (uint8_t)((mask >> 8) & 0xFF); return i2c_master_write_to_device(I2C_PORT, addr, buf, sizeof(buf), pdMS_TO_TICKS(50)); } static esp_err_t disp_clear(uint8_t addr) { uint8_t buf[17] = {0}; buf[0] = 0x00; return i2c_master_write_to_device(I2C_PORT, addr, buf, sizeof(buf), pdMS_TO_TICKS(50)); } // ======================================================= // INIT (SAFE) // ======================================================= esp_err_t display_init(void) { // por defeito: desligado s_display_enabled = false; // Se o driver I2C não estiver instalado, a primeira chamada vai dar INVALID_STATE. // Nós tratamos isso como "não há display nesta board" e seguimos sem logs. const uint8_t cmd1 = 0x21; // oscillator ON const uint8_t cmd2 = 0x81; // display ON, blink OFF const uint8_t cmd3 = 0xEF; // brightness MAX esp_err_t err; // TOP err = disp_send_cmd(DISP_TOP_ADDR, cmd1); if (err == ESP_ERR_INVALID_STATE) return ESP_OK; // I2C não instalado -> ignora if (err != ESP_OK) return ESP_OK; // display ausente/cabos -> ignora err = disp_send_cmd(DISP_TOP_ADDR, cmd2); if (err != ESP_OK) return ESP_OK; err = disp_send_cmd(DISP_TOP_ADDR, cmd3); if (err != ESP_OK) return ESP_OK; err = disp_clear(DISP_TOP_ADDR); if (err != ESP_OK) return ESP_OK; // BOTTOM err = disp_send_cmd(DISP_BOTTOM_ADDR, cmd1); if (err != ESP_OK) return ESP_OK; err = disp_send_cmd(DISP_BOTTOM_ADDR, cmd2); if (err != ESP_OK) return ESP_OK; err = disp_send_cmd(DISP_BOTTOM_ADDR, cmd3); if (err != ESP_OK) return ESP_OK; err = disp_clear(DISP_BOTTOM_ADDR); if (err != ESP_OK) return ESP_OK; // OK -> ativa display s_display_enabled = true; ESP_LOGI(TAG, "📟 Displays OK: TOP=0x%02X BOTTOM=0x%02X", DISP_TOP_ADDR, DISP_BOTTOM_ADDR); return ESP_OK; } // ======================================================= // RAW / CLEAR (com guard) // ======================================================= void display_raw_top(int pos, uint16_t mask) { if (!s_display_enabled) return; esp_err_t err = disp_raw(DISP_TOP_ADDR, pos, mask); if (err != ESP_OK) { // se o driver desaparecer a meio, desativa e cala if (err == ESP_ERR_INVALID_STATE) s_display_enabled = false; // opcional: não logar para evitar spam // ESP_LOGE(TAG, "raw_top falhou: %s", esp_err_to_name(err)); } } void display_raw_bottom(int pos, uint16_t mask) { if (!s_display_enabled) return; esp_err_t err = disp_raw(DISP_BOTTOM_ADDR, pos, mask); if (err != ESP_OK) { if (err == ESP_ERR_INVALID_STATE) s_display_enabled = false; // opcional: não logar } } void display_clear_top(void) { if (!s_display_enabled) return; esp_err_t err = disp_clear(DISP_TOP_ADDR); if (err != ESP_OK) { if (err == ESP_ERR_INVALID_STATE) s_display_enabled = false; } } void display_clear_bottom(void) { if (!s_display_enabled) return; esp_err_t err = disp_clear(DISP_BOTTOM_ADDR); if (err != ESP_OK) { if (err == ESP_ERR_INVALID_STATE) s_display_enabled = false; } } // ======================================================= // TEXTO / CHARS // ======================================================= void display_char_top(int pos, char c) { if (!s_display_enabled) return; display_raw_top(pos, charset(c)); } void display_char_bottom(int pos, char c) { if (!s_display_enabled) return; display_raw_bottom(pos, charset(c)); } void display_text_top(const char *txt) { if (!s_display_enabled) return; for (int i = 0; i < 4; i++) { char c = (txt && txt[i]) ? txt[i] : ' '; display_char_top(i, c); } } void display_text_bottom(const char *txt) { if (!s_display_enabled) return; for (int i = 0; i < 4; i++) { char c = (txt && txt[i]) ? txt[i] : ' '; display_char_bottom(i, c); } } // ======================================================= // NÚMEROS // ======================================================= static const uint16_t digit_mask[10] = { [0] = SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F, [1] = SEG_B | SEG_C, [2] = SEG_A | SEG_B | SEG_G | SEG_E | SEG_D, [3] = SEG_A | SEG_B | SEG_G | SEG_C | SEG_D, [4] = SEG_F | SEG_G | SEG_B | SEG_C, [5] = SEG_A | SEG_F | SEG_G | SEG_C | SEG_D, [6] = SEG_A | SEG_F | SEG_G | SEG_E | SEG_D | SEG_C, [7] = SEG_A | SEG_B | SEG_C, [8] = SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F | SEG_G, [9] = SEG_A | SEG_B | SEG_C | SEG_D | SEG_F | SEG_G, }; void display_digit_top(int pos, uint8_t val) { if (!s_display_enabled) return; if (val > 9) val = 0; display_raw_top(pos, digit_mask[val]); } void display_digit_bottom(int pos, uint8_t val) { if (!s_display_enabled) return; if (val > 9) val = 0; display_raw_bottom(pos, digit_mask[val]); } void display_number_top(int num) { if (!s_display_enabled) return; if (num < 0) num = 0; if (num > 9999) num = 9999; display_digit_top(3, (uint8_t)(num % 10)); display_digit_top(2, (uint8_t)((num / 10) % 10)); display_digit_top(1, (uint8_t)((num / 100) % 10)); display_digit_top(0, (uint8_t)((num / 1000) % 10)); } void display_number_bottom(int num) { if (!s_display_enabled) return; if (num < 0) num = 0; if (num > 9999) num = 9999; display_digit_bottom(3, (uint8_t)(num % 10)); display_digit_bottom(2, (uint8_t)((num / 10) % 10)); display_digit_bottom(1, (uint8_t)((num / 100) % 10)); display_digit_bottom(0, (uint8_t)((num / 1000) % 10)); } // ======================================================= // RELÓGIO (HH:MM com DP no meio) // ======================================================= void display_set_time_top(int horas, int minutos) { if (!s_display_enabled) return; if (horas < 0) horas = 0; if (horas > 99) horas = 99; if (minutos < 0) minutos = 0; if (minutos > 59) minutos = 59; int h1 = horas / 10; int h2 = horas % 10; int m1 = minutos / 10; int m2 = minutos % 10; uint16_t mid = digit_mask[h2] | SEG_DP; display_digit_top(0, (uint8_t)h1); display_raw_top(1, mid); display_digit_top(2, (uint8_t)m1); display_digit_top(3, (uint8_t)m2); } // ======================================================= // CHARSET (igual ao teu) // ======================================================= static uint16_t charset(char c) { switch (c) { // NÚMEROS case '0': return SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F; case '1': return SEG_B | SEG_C; case '2': return SEG_A | SEG_B | SEG_G | SEG_E | SEG_D; case '3': return SEG_A | SEG_B | SEG_G | SEG_C | SEG_D; case '4': return SEG_F | SEG_G | SEG_B | SEG_C; case '5': return SEG_A | SEG_F | SEG_G | SEG_C | SEG_D; case '6': return SEG_A | SEG_F | SEG_G | SEG_E | SEG_D | SEG_C; case '7': return SEG_A | SEG_B | SEG_C; case '8': return SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F | SEG_G; case '9': return SEG_A | SEG_B | SEG_C | SEG_D | SEG_F | SEG_G; // LETRAS case 'A': case 'a': return SEG_A | SEG_B | SEG_C | SEG_E | SEG_F | SEG_G; case 'B': case 'b': return SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F | SEG_G; case 'C': case 'c': return SEG_A | SEG_F | SEG_E | SEG_D; case 'D': case 'd': return SEG_B | SEG_C | SEG_D | SEG_E | SEG_G; case 'E': case 'e': return SEG_A | SEG_F | SEG_G | SEG_E | SEG_D; case 'F': case 'f': return SEG_A | SEG_F | SEG_G | SEG_E; case 'G': case 'g': return SEG_A | SEG_F | SEG_E | SEG_D | SEG_C | SEG_G; case 'H': case 'h': return SEG_F | SEG_E | SEG_G | SEG_B | SEG_C; case 'I': case 'i': return SEG_B | SEG_C; case 'J': case 'j': return SEG_B | SEG_C | SEG_D; case 'K': case 'k': return SEG_F | SEG_E | SEG_G | SEG_TR | SEG_BR; case 'L': case 'l': return SEG_F | SEG_E | SEG_D; case 'M': case 'm': return SEG_F | SEG_E | SEG_TL | SEG_TR | SEG_B | SEG_C; case 'N': case 'n': return SEG_F | SEG_E | SEG_TL | SEG_BR | SEG_C | SEG_B; case 'O': case 'o': return SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F; case 'P': case 'p': return SEG_A | SEG_B | SEG_F | SEG_G | SEG_E; case 'Q': case 'q': return SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F | SEG_BR; case 'R': case 'r': return SEG_A | SEG_B | SEG_F | SEG_G | SEG_E | SEG_BR; case 'S': case 's': return SEG_A | SEG_F | SEG_G | SEG_C | SEG_D; case 'T': case 't': return SEG_A | SEG_TM | SEG_BR; case 'U': case 'u': return SEG_F | SEG_E | SEG_D | SEG_C | SEG_B; case 'V': case 'v': return SEG_F | SEG_E | SEG_D | SEG_B | SEG_TR; case 'W': case 'w': return SEG_F | SEG_E | SEG_D | SEG_C | SEG_B | SEG_BR | SEG_TR; case 'X': case 'x': return SEG_TL | SEG_TR | SEG_ML | SEG_MR | SEG_BL | SEG_BR; case 'Y': case 'y': return SEG_F | SEG_B | SEG_G | SEG_C | SEG_D; case 'Z': case 'z': return SEG_A | SEG_TR | SEG_G | SEG_BL | SEG_D; // símbolos case '-': return SEG_G; case '.': return SEG_DP; case '_': return SEG_D; case ' ': return 0; default: return 0; } } void display_debug_segment(uint16_t bitmask) { (void)bitmask; // opcional: podes mostrar um padrão fixo se display estiver ativo // if (!display_is_enabled()) return; // display_raw_top(0, bitmask); }