2026-07-20 15:39:14 +08:00
|
|
|
#include "tft.h"
|
|
|
|
|
#include <stdbool.h>
|
|
|
|
|
#include <stddef.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
|
|
#define TFT_SPI_TIMEOUT_MS 100U
|
|
|
|
|
#define TFT_FONT_W 5U
|
|
|
|
|
#define TFT_FONT_H 7U
|
|
|
|
|
#define TFT_MAX_SCALE 4U
|
|
|
|
|
|
|
|
|
|
#if (TFT_USE_HORIZONTAL == 0U) || (TFT_USE_HORIZONTAL == 1U)
|
|
|
|
|
#define TFT_X_OFFSET 0U
|
|
|
|
|
#define TFT_Y_OFFSET 20U
|
|
|
|
|
#else
|
|
|
|
|
#define TFT_X_OFFSET 20U
|
|
|
|
|
#define TFT_Y_OFFSET 0U
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
typedef enum
|
|
|
|
|
{
|
|
|
|
|
TFT_GLYPH_SPACE = 0,
|
|
|
|
|
TFT_GLYPH_DASH,
|
|
|
|
|
TFT_GLYPH_DOT,
|
|
|
|
|
TFT_GLYPH_COLON,
|
|
|
|
|
TFT_GLYPH_0,
|
|
|
|
|
TFT_GLYPH_A = TFT_GLYPH_0 + 10
|
|
|
|
|
} TFT_GlyphIndex_e;
|
|
|
|
|
|
|
|
|
|
static bool tft_initialized = false;
|
2026-07-20 16:29:06 +08:00
|
|
|
static bool tft_transfer_ok = true;
|
2026-07-21 00:40:01 +08:00
|
|
|
static volatile bool tft_dma_active = false;
|
|
|
|
|
static TFT_TransferDoneCallback tft_dma_callback = NULL;
|
|
|
|
|
static void *tft_dma_context = NULL;
|
2026-07-20 15:39:14 +08:00
|
|
|
|
|
|
|
|
static const uint8_t tft_font5x7[][TFT_FONT_H] = {
|
|
|
|
|
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, /* space */
|
|
|
|
|
{0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00}, /* - */
|
|
|
|
|
{0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C}, /* . */
|
|
|
|
|
{0x00, 0x0C, 0x0C, 0x00, 0x0C, 0x0C, 0x00}, /* : */
|
|
|
|
|
{0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E}, /* 0 */
|
2026-07-20 21:59:16 +08:00
|
|
|
{0x04, 0x06, 0x04, 0x04, 0x04, 0x04, 0x0E}, /* 1 */
|
2026-07-20 15:39:14 +08:00
|
|
|
{0x0E, 0x11, 0x10, 0x08, 0x04, 0x02, 0x1F}, /* 2 */
|
|
|
|
|
{0x1F, 0x08, 0x04, 0x08, 0x10, 0x11, 0x0E}, /* 3 */
|
|
|
|
|
{0x08, 0x0C, 0x0A, 0x09, 0x1F, 0x08, 0x08}, /* 4 */
|
|
|
|
|
{0x1F, 0x01, 0x0F, 0x10, 0x10, 0x11, 0x0E}, /* 5 */
|
|
|
|
|
{0x0C, 0x02, 0x01, 0x0F, 0x11, 0x11, 0x0E}, /* 6 */
|
|
|
|
|
{0x1F, 0x10, 0x08, 0x04, 0x02, 0x02, 0x02}, /* 7 */
|
|
|
|
|
{0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E}, /* 8 */
|
|
|
|
|
{0x0E, 0x11, 0x11, 0x1E, 0x10, 0x08, 0x06}, /* 9 */
|
|
|
|
|
{0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11}, /* A */
|
|
|
|
|
{0x0F, 0x11, 0x11, 0x0F, 0x11, 0x11, 0x0F}, /* B */
|
|
|
|
|
{0x0E, 0x11, 0x01, 0x01, 0x01, 0x11, 0x0E}, /* C */
|
|
|
|
|
{0x0F, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0F}, /* D */
|
|
|
|
|
{0x1F, 0x01, 0x01, 0x0F, 0x01, 0x01, 0x1F}, /* E */
|
|
|
|
|
{0x1F, 0x01, 0x01, 0x0F, 0x01, 0x01, 0x01}, /* F */
|
|
|
|
|
{0x0E, 0x11, 0x01, 0x1D, 0x11, 0x11, 0x0E}, /* G */
|
|
|
|
|
{0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11}, /* H */
|
|
|
|
|
{0x0E, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E}, /* I */
|
|
|
|
|
{0x1C, 0x08, 0x08, 0x08, 0x08, 0x09, 0x06}, /* J */
|
|
|
|
|
{0x11, 0x09, 0x05, 0x03, 0x05, 0x09, 0x11}, /* K */
|
|
|
|
|
{0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x1F}, /* L */
|
|
|
|
|
{0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11}, /* M */
|
|
|
|
|
{0x11, 0x13, 0x15, 0x19, 0x11, 0x11, 0x11}, /* N */
|
|
|
|
|
{0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E}, /* O */
|
|
|
|
|
{0x0F, 0x11, 0x11, 0x0F, 0x01, 0x01, 0x01}, /* P */
|
|
|
|
|
{0x0E, 0x11, 0x11, 0x11, 0x15, 0x09, 0x16}, /* Q */
|
|
|
|
|
{0x0F, 0x11, 0x11, 0x0F, 0x05, 0x09, 0x11}, /* R */
|
|
|
|
|
{0x1E, 0x01, 0x01, 0x0E, 0x10, 0x10, 0x0F}, /* S */
|
|
|
|
|
{0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04}, /* T */
|
|
|
|
|
{0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E}, /* U */
|
|
|
|
|
{0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04}, /* V */
|
|
|
|
|
{0x11, 0x11, 0x11, 0x15, 0x15, 0x15, 0x0A}, /* W */
|
|
|
|
|
{0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11}, /* X */
|
|
|
|
|
{0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04}, /* Y */
|
|
|
|
|
{0x1F, 0x10, 0x08, 0x04, 0x02, 0x01, 0x1F}, /* Z */
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static void tft_cs_low(void)
|
|
|
|
|
{
|
|
|
|
|
HAL_GPIO_WritePin(TFT_CS_GPIO_Port, TFT_CS_Pin, GPIO_PIN_RESET);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void tft_cs_high(void)
|
|
|
|
|
{
|
|
|
|
|
HAL_GPIO_WritePin(TFT_CS_GPIO_Port, TFT_CS_Pin, GPIO_PIN_SET);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void tft_dc_command(void)
|
|
|
|
|
{
|
|
|
|
|
HAL_GPIO_WritePin(TFT_DC_GPIO_Port, TFT_DC_Pin, GPIO_PIN_RESET);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void tft_dc_data(void)
|
|
|
|
|
{
|
|
|
|
|
HAL_GPIO_WritePin(TFT_DC_GPIO_Port, TFT_DC_Pin, GPIO_PIN_SET);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void tft_reset_low(void)
|
|
|
|
|
{
|
|
|
|
|
HAL_GPIO_WritePin(TFT_RES_GPIO_Port, TFT_RES_Pin, GPIO_PIN_RESET);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void tft_reset_high(void)
|
|
|
|
|
{
|
|
|
|
|
HAL_GPIO_WritePin(TFT_RES_GPIO_Port, TFT_RES_Pin, GPIO_PIN_SET);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void tft_backlight_on(void)
|
|
|
|
|
{
|
|
|
|
|
HAL_GPIO_WritePin(TFT_BLK_GPIO_Port, TFT_BLK_Pin, GPIO_PIN_SET);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 00:40:01 +08:00
|
|
|
static void tft_dma_finish(bool ok)
|
|
|
|
|
{
|
|
|
|
|
TFT_TransferDoneCallback callback = tft_dma_callback;
|
|
|
|
|
void *context = tft_dma_context;
|
|
|
|
|
|
|
|
|
|
if (!tft_dma_active)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tft_dma_active = false;
|
|
|
|
|
tft_dma_callback = NULL;
|
|
|
|
|
tft_dma_context = NULL;
|
|
|
|
|
if (!ok)
|
|
|
|
|
{
|
|
|
|
|
tft_transfer_ok = false;
|
|
|
|
|
}
|
|
|
|
|
tft_cs_high();
|
|
|
|
|
|
|
|
|
|
if (callback != NULL)
|
|
|
|
|
{
|
|
|
|
|
callback(context, ok);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 16:29:06 +08:00
|
|
|
static void tft_bus_init(void)
|
|
|
|
|
{
|
|
|
|
|
#if TFT_USE_SOFT_SPI
|
|
|
|
|
GPIO_InitTypeDef GPIO_InitStruct = {0};
|
|
|
|
|
|
|
|
|
|
__HAL_RCC_GPIOB_CLK_ENABLE();
|
|
|
|
|
__HAL_RCC_GPIOD_CLK_ENABLE();
|
|
|
|
|
|
|
|
|
|
GPIO_InitStruct.Pin = TFT_SCK_Pin;
|
|
|
|
|
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
|
|
|
|
|
GPIO_InitStruct.Pull = GPIO_NOPULL;
|
|
|
|
|
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
|
|
|
|
|
HAL_GPIO_Init(TFT_SCK_GPIO_Port, &GPIO_InitStruct);
|
|
|
|
|
|
|
|
|
|
GPIO_InitStruct.Pin = TFT_MOSI_Pin;
|
|
|
|
|
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
|
|
|
|
|
GPIO_InitStruct.Pull = GPIO_NOPULL;
|
|
|
|
|
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
|
|
|
|
|
HAL_GPIO_Init(TFT_MOSI_GPIO_Port, &GPIO_InitStruct);
|
|
|
|
|
|
|
|
|
|
HAL_GPIO_WritePin(TFT_SCK_GPIO_Port, TFT_SCK_Pin, GPIO_PIN_SET);
|
|
|
|
|
HAL_GPIO_WritePin(TFT_MOSI_GPIO_Port, TFT_MOSI_Pin, GPIO_PIN_RESET);
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if TFT_USE_SOFT_SPI
|
|
|
|
|
static void tft_write_byte(uint8_t data)
|
|
|
|
|
{
|
|
|
|
|
for (uint8_t i = 0U; i < 8U; i++)
|
|
|
|
|
{
|
|
|
|
|
HAL_GPIO_WritePin(TFT_SCK_GPIO_Port, TFT_SCK_Pin, GPIO_PIN_RESET);
|
|
|
|
|
HAL_GPIO_WritePin(TFT_MOSI_GPIO_Port, TFT_MOSI_Pin,
|
|
|
|
|
(data & 0x80U) != 0U ? GPIO_PIN_SET : GPIO_PIN_RESET);
|
|
|
|
|
HAL_GPIO_WritePin(TFT_SCK_GPIO_Port, TFT_SCK_Pin, GPIO_PIN_SET);
|
|
|
|
|
data <<= 1U;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
2026-07-20 15:39:14 +08:00
|
|
|
static void tft_write_bytes(const uint8_t *data, uint32_t len)
|
|
|
|
|
{
|
2026-07-20 16:29:06 +08:00
|
|
|
#if TFT_USE_SOFT_SPI
|
|
|
|
|
while (len > 0U)
|
|
|
|
|
{
|
|
|
|
|
tft_write_byte(*data);
|
|
|
|
|
data++;
|
|
|
|
|
len--;
|
|
|
|
|
}
|
|
|
|
|
#else
|
2026-07-20 15:39:14 +08:00
|
|
|
while (len > 0U)
|
|
|
|
|
{
|
2026-07-20 16:41:16 +08:00
|
|
|
#if TFT_HW_SPI_BYTE_MODE && TFT_HW_SPI_PULSE_CS
|
|
|
|
|
tft_cs_low();
|
|
|
|
|
if (HAL_SPI_Transmit(&TFT_SPI_UNIT, (uint8_t *) data, 1U, TFT_SPI_TIMEOUT_MS) != HAL_OK)
|
|
|
|
|
{
|
|
|
|
|
tft_transfer_ok = false;
|
|
|
|
|
tft_cs_high();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
tft_cs_high();
|
|
|
|
|
data++;
|
|
|
|
|
len--;
|
|
|
|
|
#else
|
2026-07-20 16:29:06 +08:00
|
|
|
#if TFT_HW_SPI_BYTE_MODE
|
|
|
|
|
uint16_t chunk = 1U;
|
|
|
|
|
#else
|
2026-07-20 15:39:14 +08:00
|
|
|
uint16_t chunk = len > UINT16_MAX ? UINT16_MAX : (uint16_t) len;
|
2026-07-20 16:29:06 +08:00
|
|
|
#endif
|
|
|
|
|
if (HAL_SPI_Transmit(&TFT_SPI_UNIT, (uint8_t *) data, chunk, TFT_SPI_TIMEOUT_MS) != HAL_OK)
|
|
|
|
|
{
|
|
|
|
|
tft_transfer_ok = false;
|
|
|
|
|
tft_cs_high();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-07-20 15:39:14 +08:00
|
|
|
data += chunk;
|
|
|
|
|
len -= chunk;
|
2026-07-20 16:41:16 +08:00
|
|
|
#endif
|
2026-07-20 15:39:14 +08:00
|
|
|
}
|
2026-07-20 16:29:06 +08:00
|
|
|
#endif
|
2026-07-20 15:39:14 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-20 21:59:16 +08:00
|
|
|
static void tft_write_bytes_continuous(const uint8_t *data, uint32_t len)
|
|
|
|
|
{
|
|
|
|
|
#if TFT_USE_SOFT_SPI
|
|
|
|
|
tft_write_bytes(data, len);
|
|
|
|
|
#else
|
|
|
|
|
while (len > 0U)
|
|
|
|
|
{
|
|
|
|
|
uint16_t chunk = len > UINT16_MAX ? UINT16_MAX : (uint16_t) len;
|
|
|
|
|
if (HAL_SPI_Transmit(&TFT_SPI_UNIT, (uint8_t *) data, chunk, TFT_SPI_TIMEOUT_MS) != HAL_OK)
|
|
|
|
|
{
|
|
|
|
|
tft_transfer_ok = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
data += chunk;
|
|
|
|
|
len -= chunk;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 15:39:14 +08:00
|
|
|
static void lcd_write_reg(uint8_t reg)
|
|
|
|
|
{
|
|
|
|
|
tft_cs_low();
|
|
|
|
|
tft_dc_command();
|
|
|
|
|
tft_write_bytes(®, 1U);
|
|
|
|
|
tft_cs_high();
|
2026-07-20 16:29:06 +08:00
|
|
|
tft_dc_data();
|
2026-07-20 15:39:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void lcd_write_data8(uint8_t data)
|
|
|
|
|
{
|
|
|
|
|
tft_cs_low();
|
|
|
|
|
tft_dc_data();
|
|
|
|
|
tft_write_bytes(&data, 1U);
|
|
|
|
|
tft_cs_high();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 23:34:13 +08:00
|
|
|
static void lcd_write_reg_data(uint8_t reg, const uint8_t *data, uint32_t len)
|
2026-07-20 15:39:14 +08:00
|
|
|
{
|
|
|
|
|
tft_cs_low();
|
2026-07-20 23:34:13 +08:00
|
|
|
tft_dc_command();
|
|
|
|
|
tft_write_bytes(®, 1U);
|
|
|
|
|
if (data != NULL && len > 0U && tft_transfer_ok)
|
|
|
|
|
{
|
|
|
|
|
tft_dc_data();
|
|
|
|
|
tft_write_bytes(data, len);
|
|
|
|
|
}
|
2026-07-20 15:39:14 +08:00
|
|
|
tft_cs_high();
|
2026-07-20 23:34:13 +08:00
|
|
|
tft_dc_data();
|
2026-07-20 15:39:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void tft_write_color_block(uint16_t color, uint32_t count)
|
|
|
|
|
{
|
|
|
|
|
uint8_t buf[64];
|
|
|
|
|
uint8_t high = (uint8_t) (color >> 8);
|
|
|
|
|
uint8_t low = (uint8_t) color;
|
|
|
|
|
|
|
|
|
|
for (uint32_t i = 0U; i < sizeof(buf); i += 2U)
|
|
|
|
|
{
|
|
|
|
|
buf[i] = high;
|
|
|
|
|
buf[i + 1U] = low;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tft_cs_low();
|
|
|
|
|
tft_dc_data();
|
2026-07-20 16:29:06 +08:00
|
|
|
while (count > 0U && tft_transfer_ok)
|
2026-07-20 15:39:14 +08:00
|
|
|
{
|
|
|
|
|
uint32_t pixels = count > (sizeof(buf) / 2U) ? (sizeof(buf) / 2U) : count;
|
|
|
|
|
tft_write_bytes(buf, pixels * 2U);
|
|
|
|
|
count -= pixels;
|
|
|
|
|
}
|
|
|
|
|
tft_cs_high();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 21:59:16 +08:00
|
|
|
void TFT_WriteColorData(const uint8_t *data, uint32_t len)
|
|
|
|
|
{
|
|
|
|
|
if (data == NULL || len == 0U)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tft_cs_low();
|
|
|
|
|
tft_dc_data();
|
|
|
|
|
#if TFT_HW_SPI_STREAM_COLOR
|
|
|
|
|
tft_write_bytes_continuous(data, len);
|
|
|
|
|
#else
|
|
|
|
|
tft_write_bytes(data, len);
|
|
|
|
|
#endif
|
|
|
|
|
tft_cs_high();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 00:40:01 +08:00
|
|
|
bool TFT_WriteColorDataDMA(const uint8_t *data,
|
|
|
|
|
uint32_t len,
|
|
|
|
|
TFT_TransferDoneCallback callback,
|
|
|
|
|
void *context)
|
|
|
|
|
{
|
|
|
|
|
if (data == NULL || len == 0U || len > UINT16_MAX || tft_dma_active || TFT_SPI_UNIT.hdmatx == NULL)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tft_cs_low();
|
|
|
|
|
tft_dc_data();
|
|
|
|
|
tft_dma_callback = callback;
|
|
|
|
|
tft_dma_context = context;
|
|
|
|
|
tft_dma_active = true;
|
|
|
|
|
|
|
|
|
|
if (HAL_SPI_Transmit_DMA(&TFT_SPI_UNIT, (uint8_t *) data, (uint16_t) len) != HAL_OK)
|
|
|
|
|
{
|
|
|
|
|
tft_dma_active = false;
|
|
|
|
|
tft_dma_callback = NULL;
|
|
|
|
|
tft_dma_context = NULL;
|
|
|
|
|
tft_transfer_ok = false;
|
|
|
|
|
tft_cs_high();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
|
|
|
|
|
{
|
|
|
|
|
if (hspi == &TFT_SPI_UNIT)
|
|
|
|
|
{
|
|
|
|
|
tft_dma_finish(true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi)
|
|
|
|
|
{
|
|
|
|
|
if (hspi == &TFT_SPI_UNIT)
|
|
|
|
|
{
|
|
|
|
|
tft_dma_finish(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 21:59:16 +08:00
|
|
|
bool TFT_IsReady(void)
|
|
|
|
|
{
|
|
|
|
|
return tft_initialized && tft_transfer_ok;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 15:39:14 +08:00
|
|
|
void LCD_Address_Set(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2)
|
|
|
|
|
{
|
|
|
|
|
uint16_t xs = x1 + TFT_X_OFFSET;
|
|
|
|
|
uint16_t xe = x2 + TFT_X_OFFSET;
|
|
|
|
|
uint16_t ys = y1 + TFT_Y_OFFSET;
|
|
|
|
|
uint16_t ye = y2 + TFT_Y_OFFSET;
|
2026-07-20 23:34:13 +08:00
|
|
|
uint8_t col_data[4] = {
|
|
|
|
|
(uint8_t) (xs >> 8),
|
|
|
|
|
(uint8_t) xs,
|
|
|
|
|
(uint8_t) (xe >> 8),
|
|
|
|
|
(uint8_t) xe
|
|
|
|
|
};
|
|
|
|
|
uint8_t row_data[4] = {
|
|
|
|
|
(uint8_t) (ys >> 8),
|
|
|
|
|
(uint8_t) ys,
|
|
|
|
|
(uint8_t) (ye >> 8),
|
|
|
|
|
(uint8_t) ye
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
lcd_write_reg_data(0x2A, col_data, sizeof(col_data));
|
|
|
|
|
lcd_write_reg_data(0x2B, row_data, sizeof(row_data));
|
|
|
|
|
lcd_write_reg_data(0x2C, NULL, 0U);
|
2026-07-20 15:39:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TFT_FillRect(uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint16_t color)
|
|
|
|
|
{
|
|
|
|
|
if (x >= TFT_LCD_W || y >= TFT_LCD_H || width == 0U || height == 0U)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ((uint32_t) x + width > TFT_LCD_W)
|
|
|
|
|
{
|
|
|
|
|
width = (uint16_t) (TFT_LCD_W - x);
|
|
|
|
|
}
|
|
|
|
|
if ((uint32_t) y + height > TFT_LCD_H)
|
|
|
|
|
{
|
|
|
|
|
height = (uint16_t) (TFT_LCD_H - y);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LCD_Address_Set(x, y, (uint16_t) (x + width - 1U), (uint16_t) (y + height - 1U));
|
|
|
|
|
tft_write_color_block(color, (uint32_t) width * height);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void LCD_Fill(uint16_t xsta, uint16_t ysta, uint16_t xend, uint16_t yend, uint16_t color)
|
|
|
|
|
{
|
|
|
|
|
if (xend <= xsta || yend <= ysta)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TFT_FillRect(xsta, ysta, (uint16_t) (xend - xsta), (uint16_t) (yend - ysta), color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TFT_DrawPoint(uint16_t x, uint16_t y, uint16_t color)
|
|
|
|
|
{
|
|
|
|
|
TFT_FillRect(x, y, 1U, 1U, color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const uint8_t *tft_get_glyph(char ch)
|
|
|
|
|
{
|
|
|
|
|
if (ch >= 'a' && ch <= 'z')
|
|
|
|
|
{
|
|
|
|
|
ch = (char) (ch - ('a' - 'A'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ch >= '0' && ch <= '9')
|
|
|
|
|
{
|
|
|
|
|
return tft_font5x7[TFT_GLYPH_0 + (uint8_t) (ch - '0')];
|
|
|
|
|
}
|
|
|
|
|
if (ch >= 'A' && ch <= 'Z')
|
|
|
|
|
{
|
|
|
|
|
return tft_font5x7[TFT_GLYPH_A + (uint8_t) (ch - 'A')];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (ch)
|
|
|
|
|
{
|
|
|
|
|
case '-':
|
|
|
|
|
return tft_font5x7[TFT_GLYPH_DASH];
|
|
|
|
|
case '.':
|
|
|
|
|
return tft_font5x7[TFT_GLYPH_DOT];
|
|
|
|
|
case ':':
|
|
|
|
|
return tft_font5x7[TFT_GLYPH_COLON];
|
|
|
|
|
default:
|
|
|
|
|
return tft_font5x7[TFT_GLYPH_SPACE];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void tft_draw_char(uint16_t x, uint16_t y, char ch, uint16_t fc, uint16_t bc, uint8_t scale)
|
|
|
|
|
{
|
|
|
|
|
uint8_t line[(TFT_FONT_W * TFT_MAX_SCALE) * 2U];
|
|
|
|
|
const uint8_t *glyph;
|
|
|
|
|
uint16_t char_w;
|
|
|
|
|
uint16_t char_h;
|
|
|
|
|
|
|
|
|
|
if (scale == 0U)
|
|
|
|
|
{
|
|
|
|
|
scale = 1U;
|
|
|
|
|
}
|
|
|
|
|
if (scale > TFT_MAX_SCALE)
|
|
|
|
|
{
|
|
|
|
|
scale = TFT_MAX_SCALE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
char_w = TFT_FONT_W * scale;
|
|
|
|
|
char_h = TFT_FONT_H * scale;
|
|
|
|
|
if (x >= TFT_LCD_W || y >= TFT_LCD_H)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
glyph = tft_get_glyph(ch);
|
|
|
|
|
LCD_Address_Set(x, y, (uint16_t) (x + char_w - 1U), (uint16_t) (y + char_h - 1U));
|
|
|
|
|
|
|
|
|
|
tft_cs_low();
|
|
|
|
|
tft_dc_data();
|
|
|
|
|
for (uint8_t row = 0U; row < TFT_FONT_H; row++)
|
|
|
|
|
{
|
|
|
|
|
for (uint8_t repeat_y = 0U; repeat_y < scale; repeat_y++)
|
|
|
|
|
{
|
|
|
|
|
uint32_t idx = 0U;
|
|
|
|
|
for (uint8_t col = 0U; col < TFT_FONT_W; col++)
|
|
|
|
|
{
|
2026-07-20 16:29:06 +08:00
|
|
|
uint16_t color = (glyph[row] & (1U << col)) ? fc : bc;
|
2026-07-20 15:39:14 +08:00
|
|
|
for (uint8_t repeat_x = 0U; repeat_x < scale; repeat_x++)
|
|
|
|
|
{
|
|
|
|
|
line[idx++] = (uint8_t) (color >> 8);
|
|
|
|
|
line[idx++] = (uint8_t) color;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
tft_write_bytes(line, idx);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
tft_cs_high();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TFT_DrawString(uint16_t x, uint16_t y, const char *text, uint16_t fc, uint16_t bc, uint8_t scale)
|
|
|
|
|
{
|
|
|
|
|
uint16_t cursor = x;
|
|
|
|
|
uint16_t advance;
|
|
|
|
|
|
|
|
|
|
if (text == NULL)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (scale == 0U)
|
|
|
|
|
{
|
|
|
|
|
scale = 1U;
|
|
|
|
|
}
|
|
|
|
|
if (scale > TFT_MAX_SCALE)
|
|
|
|
|
{
|
|
|
|
|
scale = TFT_MAX_SCALE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
advance = (TFT_FONT_W + 1U) * scale;
|
|
|
|
|
while (*text != '\0')
|
|
|
|
|
{
|
|
|
|
|
if (cursor + (TFT_FONT_W * scale) >= TFT_LCD_W)
|
|
|
|
|
{
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tft_draw_char(cursor, y, *text, fc, bc, scale);
|
|
|
|
|
cursor = (uint16_t) (cursor + advance);
|
|
|
|
|
text++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TFT_Clear(uint16_t color)
|
|
|
|
|
{
|
|
|
|
|
LCD_Fill(0U, 0U, TFT_LCD_W, TFT_LCD_H, color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void LCD_Init(void)
|
|
|
|
|
{
|
|
|
|
|
if (tft_initialized)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 16:29:06 +08:00
|
|
|
tft_transfer_ok = true;
|
|
|
|
|
tft_bus_init();
|
2026-07-20 15:39:14 +08:00
|
|
|
tft_cs_high();
|
|
|
|
|
tft_reset_low();
|
|
|
|
|
HAL_Delay(100);
|
|
|
|
|
tft_reset_high();
|
|
|
|
|
HAL_Delay(100);
|
|
|
|
|
|
|
|
|
|
tft_backlight_on();
|
|
|
|
|
HAL_Delay(100);
|
|
|
|
|
|
|
|
|
|
lcd_write_reg(0x11);
|
|
|
|
|
HAL_Delay(120);
|
|
|
|
|
|
|
|
|
|
lcd_write_reg(0x36);
|
|
|
|
|
#if TFT_USE_HORIZONTAL == 0U
|
|
|
|
|
lcd_write_data8(0x00);
|
|
|
|
|
#elif TFT_USE_HORIZONTAL == 1U
|
|
|
|
|
lcd_write_data8(0xC0);
|
|
|
|
|
#elif TFT_USE_HORIZONTAL == 2U
|
|
|
|
|
lcd_write_data8(0x70);
|
|
|
|
|
#else
|
|
|
|
|
lcd_write_data8(0xA0);
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
lcd_write_reg(0x3A);
|
|
|
|
|
lcd_write_data8(0x05);
|
|
|
|
|
|
|
|
|
|
lcd_write_reg(0xB2);
|
|
|
|
|
lcd_write_data8(0x0C);
|
|
|
|
|
lcd_write_data8(0x0C);
|
|
|
|
|
lcd_write_data8(0x00);
|
|
|
|
|
lcd_write_data8(0x33);
|
|
|
|
|
lcd_write_data8(0x33);
|
|
|
|
|
|
|
|
|
|
lcd_write_reg(0xB7);
|
|
|
|
|
lcd_write_data8(0x35);
|
|
|
|
|
|
|
|
|
|
lcd_write_reg(0xBB);
|
|
|
|
|
lcd_write_data8(0x32);
|
|
|
|
|
|
|
|
|
|
lcd_write_reg(0xC2);
|
|
|
|
|
lcd_write_data8(0x01);
|
|
|
|
|
|
|
|
|
|
lcd_write_reg(0xC3);
|
|
|
|
|
lcd_write_data8(0x15);
|
|
|
|
|
|
|
|
|
|
lcd_write_reg(0xC4);
|
|
|
|
|
lcd_write_data8(0x20);
|
|
|
|
|
|
|
|
|
|
lcd_write_reg(0xC6);
|
|
|
|
|
lcd_write_data8(0x0F);
|
|
|
|
|
|
|
|
|
|
lcd_write_reg(0xD0);
|
|
|
|
|
lcd_write_data8(0xA4);
|
|
|
|
|
lcd_write_data8(0xA1);
|
|
|
|
|
|
|
|
|
|
lcd_write_reg(0xE0);
|
|
|
|
|
lcd_write_data8(0xD0);
|
|
|
|
|
lcd_write_data8(0x08);
|
|
|
|
|
lcd_write_data8(0x0E);
|
|
|
|
|
lcd_write_data8(0x09);
|
|
|
|
|
lcd_write_data8(0x09);
|
|
|
|
|
lcd_write_data8(0x05);
|
|
|
|
|
lcd_write_data8(0x31);
|
|
|
|
|
lcd_write_data8(0x33);
|
|
|
|
|
lcd_write_data8(0x48);
|
|
|
|
|
lcd_write_data8(0x17);
|
|
|
|
|
lcd_write_data8(0x14);
|
|
|
|
|
lcd_write_data8(0x15);
|
|
|
|
|
lcd_write_data8(0x31);
|
|
|
|
|
lcd_write_data8(0x34);
|
|
|
|
|
|
|
|
|
|
lcd_write_reg(0xE1);
|
|
|
|
|
lcd_write_data8(0xD0);
|
|
|
|
|
lcd_write_data8(0x08);
|
|
|
|
|
lcd_write_data8(0x0E);
|
|
|
|
|
lcd_write_data8(0x09);
|
|
|
|
|
lcd_write_data8(0x09);
|
|
|
|
|
lcd_write_data8(0x15);
|
|
|
|
|
lcd_write_data8(0x31);
|
|
|
|
|
lcd_write_data8(0x33);
|
|
|
|
|
lcd_write_data8(0x48);
|
|
|
|
|
lcd_write_data8(0x17);
|
|
|
|
|
lcd_write_data8(0x14);
|
|
|
|
|
lcd_write_data8(0x15);
|
|
|
|
|
lcd_write_data8(0x31);
|
|
|
|
|
lcd_write_data8(0x34);
|
|
|
|
|
|
|
|
|
|
lcd_write_reg(0x21);
|
|
|
|
|
lcd_write_reg(0x29);
|
|
|
|
|
|
|
|
|
|
TFT_Clear(TFT_COLOR_BLACK);
|
2026-07-20 16:29:06 +08:00
|
|
|
if (tft_transfer_ok)
|
|
|
|
|
{
|
|
|
|
|
tft_initialized = true;
|
|
|
|
|
}
|
2026-07-20 15:39:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TFT_Init(void)
|
|
|
|
|
{
|
|
|
|
|
LCD_Init();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 16:29:06 +08:00
|
|
|
void TFT_ShowBootScreen(void)
|
|
|
|
|
{
|
|
|
|
|
if (!tft_initialized)
|
|
|
|
|
{
|
|
|
|
|
TFT_Init();
|
|
|
|
|
if (!tft_initialized)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tft_transfer_ok = true;
|
2026-07-21 22:23:40 +08:00
|
|
|
TFT_Clear(TFT_COLOR_BLACK);
|
|
|
|
|
TFT_DrawString(20U, 80U, "TronOneH7-Scaffold", TFT_COLOR_WHITE, TFT_COLOR_BLACK, 2U);
|
|
|
|
|
TFT_DrawString(107U, 130U, "Starting...", TFT_COLOR_WHITE, TFT_COLOR_BLACK, 1U);
|
2026-07-20 16:29:06 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-20 15:39:14 +08:00
|
|
|
const char *TFT_RobotModeText(RobotMode_t mode)
|
|
|
|
|
{
|
|
|
|
|
switch (mode)
|
|
|
|
|
{
|
|
|
|
|
case NORMAL_MODE:
|
|
|
|
|
return "NORMAL";
|
|
|
|
|
case SYS_ERROR_OCCURRED:
|
|
|
|
|
return "ERROR";
|
|
|
|
|
case REMOTE_NOT_CONNECTED:
|
|
|
|
|
return "REMOTE OFF";
|
2026-07-21 00:40:01 +08:00
|
|
|
case REMOTE_NOT_READY:
|
|
|
|
|
return "NOT READY";
|
|
|
|
|
case REMOTE_READY:
|
|
|
|
|
return "READY";
|
|
|
|
|
case REMOTE_PROTECT:
|
|
|
|
|
return "PROTECT";
|
2026-07-20 15:39:14 +08:00
|
|
|
case AUTO_SHOOTING_MODE:
|
|
|
|
|
return "AUTO SHOOT";
|
|
|
|
|
case IMU_CALIBERATION_MODE:
|
|
|
|
|
return "IMU CAL";
|
|
|
|
|
default:
|
|
|
|
|
return "UNKNOWN";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static uint16_t tft_mode_color(RobotMode_t mode)
|
|
|
|
|
{
|
|
|
|
|
switch (mode)
|
|
|
|
|
{
|
|
|
|
|
case NORMAL_MODE:
|
|
|
|
|
return TFT_COLOR_GREEN;
|
|
|
|
|
case SYS_ERROR_OCCURRED:
|
|
|
|
|
case REMOTE_NOT_CONNECTED:
|
|
|
|
|
return TFT_COLOR_RED;
|
2026-07-21 00:40:01 +08:00
|
|
|
case REMOTE_NOT_READY:
|
|
|
|
|
return TFT_COLOR_WHITE;
|
|
|
|
|
case REMOTE_READY:
|
|
|
|
|
return TFT_COLOR_MAGENTA;
|
|
|
|
|
case REMOTE_PROTECT:
|
2026-07-20 15:39:14 +08:00
|
|
|
return TFT_COLOR_YELLOW;
|
|
|
|
|
case AUTO_SHOOTING_MODE:
|
|
|
|
|
return TFT_COLOR_CYAN;
|
|
|
|
|
case IMU_CALIBERATION_MODE:
|
|
|
|
|
return TFT_COLOR_MAGENTA;
|
|
|
|
|
default:
|
|
|
|
|
return TFT_COLOR_LIGHT_GRAY;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int16_t tft_temperature_to_tenths(float temperature_c)
|
|
|
|
|
{
|
|
|
|
|
return (int16_t) ((temperature_c * 10.0f) + (temperature_c >= 0.0f ? 0.5f : -0.5f));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void tft_format_temperature(char *buf, size_t len, int16_t temperature_tenths)
|
|
|
|
|
{
|
|
|
|
|
uint16_t abs_temp;
|
2026-07-20 16:29:06 +08:00
|
|
|
uint16_t integer;
|
|
|
|
|
uint8_t decimal;
|
|
|
|
|
size_t idx = 0U;
|
|
|
|
|
char digits[5];
|
|
|
|
|
uint8_t digits_len = 0U;
|
2026-07-20 15:39:14 +08:00
|
|
|
|
2026-07-20 16:29:06 +08:00
|
|
|
if (buf == NULL || len < 6U)
|
2026-07-20 15:39:14 +08:00
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (temperature_tenths < 0)
|
|
|
|
|
{
|
|
|
|
|
abs_temp = (uint16_t) (-temperature_tenths);
|
2026-07-20 16:29:06 +08:00
|
|
|
buf[idx++] = '-';
|
2026-07-20 15:39:14 +08:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
abs_temp = (uint16_t) temperature_tenths;
|
|
|
|
|
}
|
2026-07-20 16:29:06 +08:00
|
|
|
|
|
|
|
|
integer = abs_temp / 10U;
|
|
|
|
|
decimal = (uint8_t) (abs_temp % 10U);
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
digits[digits_len++] = (char) ('0' + (integer % 10U));
|
|
|
|
|
integer /= 10U;
|
|
|
|
|
} while (integer > 0U && digits_len < sizeof(digits));
|
|
|
|
|
|
|
|
|
|
while (digits_len > 0U && idx + 1U < len)
|
|
|
|
|
{
|
|
|
|
|
buf[idx++] = digits[--digits_len];
|
|
|
|
|
}
|
|
|
|
|
if (idx + 4U < len)
|
|
|
|
|
{
|
|
|
|
|
buf[idx++] = '.';
|
|
|
|
|
buf[idx++] = (char) ('0' + decimal);
|
|
|
|
|
buf[idx++] = ' ';
|
|
|
|
|
buf[idx++] = 'C';
|
|
|
|
|
}
|
|
|
|
|
buf[idx] = '\0';
|
2026-07-20 15:39:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void tft_draw_status_layout(void)
|
|
|
|
|
{
|
|
|
|
|
TFT_Clear(TFT_COLOR_BLACK);
|
|
|
|
|
TFT_DrawString(16U, 16U, "DM LCD", TFT_COLOR_CYAN, TFT_COLOR_BLACK, 3U);
|
|
|
|
|
TFT_FillRect(16U, 54U, 248U, 2U, TFT_COLOR_DARK_GRAY);
|
|
|
|
|
TFT_DrawString(16U, 76U, "TEMP:", TFT_COLOR_YELLOW, TFT_COLOR_BLACK, 2U);
|
|
|
|
|
TFT_DrawString(16U, 132U, "MODE:", TFT_COLOR_YELLOW, TFT_COLOR_BLACK, 2U);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TFT_ShowStatus(float temperature_c, RobotMode_t mode)
|
|
|
|
|
{
|
|
|
|
|
static bool layout_ready = false;
|
|
|
|
|
static int16_t last_temperature_tenths = INT16_MIN;
|
|
|
|
|
static RobotMode_t last_mode = (RobotMode_t) 0xFF;
|
|
|
|
|
int16_t temperature_tenths = tft_temperature_to_tenths(temperature_c);
|
|
|
|
|
char temperature_text[16];
|
|
|
|
|
const char *mode_text = TFT_RobotModeText(mode);
|
|
|
|
|
|
|
|
|
|
if (!tft_initialized)
|
|
|
|
|
{
|
|
|
|
|
TFT_Init();
|
2026-07-20 16:29:06 +08:00
|
|
|
if (!tft_initialized)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-07-20 15:39:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!layout_ready)
|
|
|
|
|
{
|
2026-07-20 16:29:06 +08:00
|
|
|
tft_transfer_ok = true;
|
2026-07-20 15:39:14 +08:00
|
|
|
tft_draw_status_layout();
|
2026-07-20 16:29:06 +08:00
|
|
|
if (!tft_transfer_ok)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-07-20 15:39:14 +08:00
|
|
|
layout_ready = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (temperature_tenths != last_temperature_tenths)
|
|
|
|
|
{
|
2026-07-20 16:29:06 +08:00
|
|
|
tft_transfer_ok = true;
|
2026-07-20 15:39:14 +08:00
|
|
|
uint16_t temperature_color = temperature_tenths >= 550 ? TFT_COLOR_RED : TFT_COLOR_WHITE;
|
|
|
|
|
tft_format_temperature(temperature_text, sizeof(temperature_text), temperature_tenths);
|
|
|
|
|
TFT_FillRect(96U, 68U, 168U, 32U, TFT_COLOR_BLACK);
|
|
|
|
|
TFT_DrawString(100U, 70U, temperature_text, temperature_color, TFT_COLOR_BLACK, 3U);
|
2026-07-20 16:29:06 +08:00
|
|
|
if (tft_transfer_ok)
|
|
|
|
|
{
|
|
|
|
|
last_temperature_tenths = temperature_tenths;
|
|
|
|
|
}
|
2026-07-20 15:39:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mode != last_mode)
|
|
|
|
|
{
|
2026-07-20 16:29:06 +08:00
|
|
|
tft_transfer_ok = true;
|
2026-07-20 15:39:14 +08:00
|
|
|
TFT_FillRect(96U, 124U, 184U, 32U, TFT_COLOR_BLACK);
|
|
|
|
|
TFT_DrawString(100U, 126U, mode_text, tft_mode_color(mode), TFT_COLOR_BLACK, 3U);
|
2026-07-20 16:29:06 +08:00
|
|
|
if (tft_transfer_ok)
|
|
|
|
|
{
|
|
|
|
|
last_mode = mode;
|
|
|
|
|
}
|
2026-07-20 15:39:14 +08:00
|
|
|
}
|
|
|
|
|
}
|