GD32H759 + RT-Thread 工控实战--第4篇 SDRAM,SDIO,以及触摸屏

GD32H759 + RT-Thread 工控实战--第4篇 SDRAM,SDIO,以及触摸屏

@TOC


前言

本篇将实现在GD32H7xx全功能评估板上实现 SDRAM,SDIO+FatFS,TLI + GT911触摸屏。之所以这次要一起实现这么多内容是想将评估板实现成一个某种带触摸功能可显示的、可存储的"仪器",为了实现sdio高速读写、LCD显示,SDRAM也是必须要调通的一部分。由于上一篇文章写完后让K老师审核校对,导致正文内容被加入了非常多"豆包"味,所以这篇我就不让K老师帮忙审核了。阅读起来可能会有些语句不通顺之类的问题。

一、调研

1.1、实验设计

本次实验设计为用LCD显示ADC输入波形,同时用tf卡记录采样数据,以及手指触摸屏幕暂停采集,点击继续采集。

1.2、资料研究

由于本次要解决的问题较多,所以阅读花了较长时间。以下是阅读记录:

资料 用途与解读
GD32H759I-EVAL评估板使用指南_Rev2.2.pdf 跳线定义、原理图
GD32H759xx Datasheet_Rev2.3.pdf 引脚复用表、EXMC/SDIO/TLI 外设章节
demo suites: 16_EXMC_SDRAM,18_SDIO_SDardTest,24_TLI_IPA,29_TLI_Touch_Draw 阅读源码理解使用步骤
RT-Thread 代码:dev_mmcsd_core.c/.h,mmcsd_host.h/blk_partition.c,partitions/efi.c 主要阅读用以了解SDIO驱动框架

1.3、几个结论

来源 结论
SDRAM EXMC DEVICE0,32MB @ 0xC0000000,跳线 JP40=(1,2) demo程序 基本上可以无痛抄袭rt-thread固件库中的代码
SDIO实例 SDIO0:CLK=PC12、CMD=PD2、D0=PB13、D1~D3=PC9/10/11,全 AF12 datasheet gd32H7xx sdio是全新IP,有2个实例,列出的实例0,需要重新写驱动,驱动中会实现0或1宏配置
SDIO时钟 PLL1R=200MHz 直供,直接分频(400k=500,25M=8,50M=4) demo中的rcu_config 只能参考demo,不能参考rt-thread中的现有驱动代码
SDIO缺陷 评估板SDIO卡座CD脚未接入MCU,热插拔事件不会发生 原理图 我会在驱动中提供手工重新发现卡命令sdreprobe------模拟热插拔功能
LCD 480×272 RGB 并口,HSW=41/HBP=2/HFP=2、VSW=10/VBP=2/VFP=2,全低有效 lcd demo程序
TLI 时钟 PLL2R=64MHz ÷8 = 8MHz 像素钟(TLI 时钟无源选择寄存器,硬件固定 PLL2R/div) 完全从demo中分析
背光 拉高PG13 lcd demo gpio操作
触摸 GT 911, i2c:SCL=PH7、SDA=PF9、RST=PF6、INT=PH6,地址 0xBA lcd demo 纯i2c操作,相对简单

1.4、几个冲突

引脚 TLI功能 冲突方 决策和理由
PC11 PCD_B4 SDIO_D3 决策为选择到LCD,原因是SDIO仍可用,但会退行到1-bit模式(速度会下降)
PG11/PG13/PG14 LCD_B3/背光/LCD_B0 enet RMII 决策为选择LCD,原因是本次实验并不需要enet
PA5 LCD_R4 DAC0_OUT1 决策为选择LCD,原因是本次实验,可以不用到DAC

1.5、大量的跳线

LCD: JP41 / JP43 / JP44 / JP45 / JP46 / JP47 / JP48 / JP49 / JP50 / JP51 / JP52 / JP53 / JP54 / JP55 / JP56 / JP57 / JP58 / JP59 看丝印,全部跳到LCD SDIO:JP65=(2,3) 、JP62=(1,2)

二、驱动编写

2.1、前置研究

  1. rt-thread mmcsd框架 SDIO并不是 rt_device的"派生",而是实现 mmcsd框架下的rt_mmcsd_host_ops的三个回调(request,set_iocfg,get_card_status)。需要注意的是 mmcsd这个框架的 "sem_ack"机制,mmcsd框架在调用 ops->request后会等待 sem_ack信号量。所以我们在实现request回调函数时,必须要用 mmcsd_req_complete(host)来释放信号量。否则 mmcsd_detect过程会卡死掉。
  2. 外设的DMA规划和处理。 SDRAM/TLI: 可以用取巧的办法,将帧缓冲放在 MPU non-cacheable区域。 CPU写入与 TLI DMA读取直接具备一致性,无需额外的维护成本。关于SDIO 目前使用了驱动维护 32Bit对齐的 bounce buffer,IDMA仅处理这个保留buffer。尽管目前在 bounce buffer和"用户buffer"之间仍在rt_memcpy,但其性能是可以接受的。如果对sdio读写有更严格的要求,那么需要重新考虑驱动在DMA这块的实现。

2.2、驱动代码

2.2.1、SDRAM区动

~/gd32/rt-thread/bsp/gd32/arm/libraries/gd32_drivers/drv_sdram_h7.c

c 复制代码
/*
 * GD32H7 SDRAM 初始化 ------ EXMC DEVICE0, MT48LC16M16A2 (32MB @ 0xC0000000)
 * RT-Thread 集成: INIT_BOARD_EXPORT 上电自动初始化
 *
 * 参数来源: 官方例程 16_EXMC_SDRAM (13行/9列/16bit/4bank/CL3/SDCLK=CK_EXMC/3)
 * 跳线: JP40 接 (1,2)  ((2,3) 是 I2S, 互斥)
 *
 * MPU: 0xC0000000 起 32MB 配为 Normal Non-Cacheable Shareable + XN。
 *   bring-up 阶段不用 cache, 杜绝一切一致性问题;
 *   后续 LCD 帧缓冲再按需改 Write-Back + cache 维护。
 *   MPU 区域号自动挑第一个未使能的, 不与 ENET 的 MPU 区冲突。
 */

#include <rtthread.h>
#include "gd32h7xx.h"

#define DBG_TAG "drv.sdram"
#define DBG_LVL DBG_INFO
#include <rtdbg.h>

#if defined(BSP_USING_SDRAM) && defined(SOC_SERIES_GD32H7xx)

#define SDRAM_DEVICE0_ADDR   ((uint32_t)0xC0000000U)
#define SDRAM_SIZE           (32U * 1024U * 1024U)
#define SDRAM_TIMEOUT        ((uint32_t)0x0000FFFFU)

/* mode register 位定义(来自官方例程) */
#define SDRAM_MODEREG_BURST_LENGTH_1         ((uint16_t)0x0000U)
#define SDRAM_MODEREG_BURST_TYPE_SEQUENTIAL  ((uint16_t)0x0000U)
#define SDRAM_MODEREG_CAS_LATENCY_3          ((uint16_t)0x0030U)
#define SDRAM_MODEREG_OPERATING_MODE_STANDARD ((uint16_t)0x0000U)
#define SDRAM_MODEREG_WRITEBURST_MODE_SINGLE ((uint16_t)0x0200U)

static void sdram_gpio_init(void)
{
    rcu_periph_clock_enable(RCU_GPIOC);
    rcu_periph_clock_enable(RCU_GPIOD);
    rcu_periph_clock_enable(RCU_GPIOE);
    rcu_periph_clock_enable(RCU_GPIOF);
    rcu_periph_clock_enable(RCU_GPIOG);
    rcu_periph_clock_enable(RCU_GPIOH);

    /* SDNE0(PC2), SDCKE0(PC3) */
    gpio_af_set(GPIOC, GPIO_AF_12, GPIO_PIN_2 | GPIO_PIN_3);
    gpio_mode_set(GPIOC, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_2 | GPIO_PIN_3);
    gpio_output_options_set(GPIOC, GPIO_OTYPE_PP, GPIO_OSPEED_85MHZ, GPIO_PIN_2 | GPIO_PIN_3);

    /* D12(PC0) */
    gpio_af_set(GPIOC, GPIO_AF_1, GPIO_PIN_0);
    gpio_mode_set(GPIOC, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_0);
    gpio_output_options_set(GPIOC, GPIO_OTYPE_PP, GPIO_OSPEED_85MHZ, GPIO_PIN_0);

    /* D2(PD0),D3(PD1),D13(PD8),D14(PD9),D15(PD10),D0(PD14),D1(PD15) */
    gpio_af_set(GPIOD, GPIO_AF_12, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_8 | GPIO_PIN_9 |
                GPIO_PIN_10 | GPIO_PIN_14 | GPIO_PIN_15);
    gpio_mode_set(GPIOD, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_8 | GPIO_PIN_9 |
                GPIO_PIN_10 | GPIO_PIN_14 | GPIO_PIN_15);
    gpio_output_options_set(GPIOD, GPIO_OTYPE_PP, GPIO_OSPEED_85MHZ, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_8 | GPIO_PIN_9 |
                            GPIO_PIN_10 | GPIO_PIN_14 | GPIO_PIN_15);

    /* NBL0(PE0),NBL1(PE1),D4..D11(PE7..PE14) */
    gpio_af_set(GPIOE, GPIO_AF_12, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_7 | GPIO_PIN_8 |
                GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14);
    gpio_mode_set(GPIOE, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_7 | GPIO_PIN_8 |
                GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14);
    gpio_output_options_set(GPIOE, GPIO_OTYPE_PP, GPIO_OSPEED_85MHZ, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_7 | GPIO_PIN_8 |
                            GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14);

    /* A0..A5(PF0..PF5), NRAS(PF11), A6..A9(PF12..PF15) */
    gpio_af_set(GPIOF, GPIO_AF_12, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 |
                GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_11 | GPIO_PIN_12 |
                GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15);
    gpio_mode_set(GPIOF, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 |
                GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_11 | GPIO_PIN_12 |
                GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15);
    gpio_output_options_set(GPIOF, GPIO_OTYPE_PP, GPIO_OSPEED_85MHZ, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 |
                            GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_11 | GPIO_PIN_12 |
                            GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15);

    /* A10(PG0),A11(PG1),A12(PG2),BA0(PG4),BA1(PG5),SDCLK(PG8),NCAS(PG15) */
    gpio_af_set(GPIOG, GPIO_AF_12, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_4 |
                GPIO_PIN_5 | GPIO_PIN_8 | GPIO_PIN_15);
    gpio_mode_set(GPIOG, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_4 |
                GPIO_PIN_5 | GPIO_PIN_8 | GPIO_PIN_15);
    gpio_output_options_set(GPIOG, GPIO_OTYPE_PP, GPIO_OSPEED_85MHZ, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_4 |
                GPIO_PIN_5 | GPIO_PIN_8 | GPIO_PIN_15);

    /* SDNWE(PH5) */
    gpio_af_set(GPIOH, GPIO_AF_12, GPIO_PIN_5);
    gpio_mode_set(GPIOH, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_5);
    gpio_output_options_set(GPIOH, GPIO_OTYPE_PP, GPIO_OSPEED_85MHZ, GPIO_PIN_5);
}

/* 在 MPU 里找第一个未使能的区域, 配为 Normal Non-Cacheable Shareable XN */
static rt_err_t sdram_mpu_config(void)
{
    rt_uint32_t rnr;
    rt_uint32_t rasr;

    for (rnr = 0; rnr < 16U; rnr++) {
        MPU->RNR = rnr;
        if ((MPU->RASR & MPU_RASR_ENABLE_Msk) == 0U) {
            break;
        }
    }
    if (rnr == 16U) {
        LOG_E("no free MPU region");
        return -RT_EFULL;
    }

    /* RASR: XN=1, AP=full(0b011), TEX=001(normal non-cacheable), S=1, C=0, B=0,
       SIZE=24 (32MB -> 2^(24+1)), ENABLE=1 */
    rasr = (1UL << MPU_RASR_XN_Pos)                    |
           (3UL << MPU_RASR_AP_Pos)                    |
           (1UL << MPU_RASR_TEX_Pos)                   |
           (1UL << MPU_RASR_S_Pos)                     |
           ((24UL << MPU_RASR_SIZE_Pos) & MPU_RASR_SIZE_Msk) |
           MPU_RASR_ENABLE_Msk;

    MPU->RNR  = rnr;
    MPU->RBAR = SDRAM_DEVICE0_ADDR;
    MPU->RASR = rasr;

    __DSB();
    __ISB();

    LOG_I("MPU region %lu: 0xC0000000 32MB normal non-cacheable", rnr);
    return RT_EOK;
}

static int h7_sdram_init(void)
{
    exmc_sdram_parameter_struct sdram_init_struct;
    exmc_sdram_timing_parameter_struct sdram_timing_init_struct;
    exmc_sdram_command_parameter_struct sdram_command_init_struct;
    uint32_t command_content, bank_select, timeout;
    volatile rt_uint32_t *probe;

    rcu_periph_clock_enable(RCU_EXMC);
    sdram_gpio_init();

    if (sdram_mpu_config() != RT_EOK) {
        return -RT_ERROR;
    }

    bank_select = EXMC_SDRAM_DEVICE0_SELECT;

    /* step 1: 时序寄存器 */
    sdram_timing_init_struct.load_mode_register_delay  = 2;
    sdram_timing_init_struct.exit_selfrefresh_delay    = 12;
    sdram_timing_init_struct.row_address_select_delay  = 8;
    sdram_timing_init_struct.auto_refresh_delay        = 11;
    sdram_timing_init_struct.write_recovery_delay      = 2;
    sdram_timing_init_struct.row_precharge_delay       = 4;
    sdram_timing_init_struct.row_to_column_delay       = 4;

    /* step 2: 控制寄存器 */
    exmc_sdram_struct_para_init(&sdram_init_struct);
    sdram_init_struct.sdram_device          = EXMC_SDRAM_DEVICE0;
    sdram_init_struct.column_address_width  = EXMC_SDRAM_COW_ADDRESS_9;
    sdram_init_struct.row_address_width     = EXMC_SDRAM_ROW_ADDRESS_13;
    sdram_init_struct.data_width            = EXMC_SDRAM_DATABUS_WIDTH_16B;
    sdram_init_struct.internal_bank_number  = EXMC_SDRAM_4_INTER_BANK;
    sdram_init_struct.cas_latency           = EXMC_CAS_LATENCY_3_SDCLK;
    sdram_init_struct.write_protection      = DISABLE;
    sdram_init_struct.sdclock_config        = EXMC_SDCLK_PERIODS_3_CK_EXMC;
    sdram_init_struct.burst_read_switch     = ENABLE;
    sdram_init_struct.pipeline_read_delay   = EXMC_PIPELINE_DELAY_1_CK_EXMC;
    sdram_init_struct.timing                = &sdram_timing_init_struct;
    exmc_sdram_init(&sdram_init_struct);

    /* step 3: CKE 拉高 */
    sdram_command_init_struct.command              = EXMC_SDRAM_CLOCK_ENABLE;
    sdram_command_init_struct.bank_select          = bank_select;
    sdram_command_init_struct.auto_refresh_number  = EXMC_SDRAM_AUTO_REFLESH_1_SDCLK;
    sdram_command_init_struct.mode_register_content = 0;
    timeout = SDRAM_TIMEOUT;
    while ((exmc_flag_get(EXMC_SDRAM_DEVICE0, EXMC_SDRAM_FLAG_NREADY) != RESET) && (timeout > 0)) {
        timeout--;
    }
    exmc_sdram_command_config(&sdram_command_init_struct);

    /* step 4: 等待 10ms(600MHz 下约 100000 循环/ms) */
    {
        volatile uint32_t i;
        for (i = 0; i < 6000000U; i++) {
        }
    }

    /* step 5: precharge all */
    sdram_command_init_struct.command = EXMC_SDRAM_PRECHARGE_ALL;
    timeout = SDRAM_TIMEOUT;
    while ((exmc_flag_get(EXMC_SDRAM_DEVICE0, EXMC_SDRAM_FLAG_NREADY) != RESET) && (timeout > 0)) {
        timeout--;
    }
    exmc_sdram_command_config(&sdram_command_init_struct);

    /* step 6: auto-refresh x8 */
    sdram_command_init_struct.command             = EXMC_SDRAM_AUTO_REFRESH;
    sdram_command_init_struct.auto_refresh_number = EXMC_SDRAM_AUTO_REFLESH_8_SDCLK;
    timeout = SDRAM_TIMEOUT;
    while ((exmc_flag_get(EXMC_SDRAM_DEVICE0, EXMC_SDRAM_FLAG_NREADY) != RESET) && (timeout > 0)) {
        timeout--;
    }
    exmc_sdram_command_config(&sdram_command_init_struct);

    /* step 7: load mode register (burst=1, sequential, CL3, single write) */
    command_content = (uint32_t)SDRAM_MODEREG_BURST_LENGTH_1        |
                      SDRAM_MODEREG_BURST_TYPE_SEQUENTIAL   |
                      SDRAM_MODEREG_CAS_LATENCY_3           |
                      SDRAM_MODEREG_OPERATING_MODE_STANDARD |
                      SDRAM_MODEREG_WRITEBURST_MODE_SINGLE;
    sdram_command_init_struct.command              = EXMC_SDRAM_LOAD_MODE_REGISTER;
    sdram_command_init_struct.auto_refresh_number  = EXMC_SDRAM_AUTO_REFLESH_1_SDCLK;
    sdram_command_init_struct.mode_register_content = command_content;
    timeout = SDRAM_TIMEOUT;
    while ((exmc_flag_get(EXMC_SDRAM_DEVICE0, EXMC_SDRAM_FLAG_NREADY) != RESET) && (timeout > 0)) {
        timeout--;
    }
    exmc_sdram_command_config(&sdram_command_init_struct);
  
    /* step 8: 刷新计数 1542 (64ms/8192, SDCLK=CK_EXMC/3) */
    exmc_sdram_refresh_count_set(1542);
    timeout = SDRAM_TIMEOUT;
    while ((exmc_flag_get(EXMC_SDRAM_DEVICE0, EXMC_SDRAM_FLAG_NREADY) != RESET) && (timeout > 0)) {
        timeout--;
    }

    /* 上电自检: 写一个值读回来 */
    probe = (volatile rt_uint32_t *)SDRAM_DEVICE0_ADDR;
    *probe = 0x5A5A1234UL;
    if (*probe == 0x5A5A1234UL) {
        LOG_I("SDRAM init ok, 32MB @ 0xC0000000");
        return RT_EOK;
    }
    LOG_E("SDRAM probe failed: wrote 0x5A5A1234, read 0x%08lx (check JP40=(1,2))", *probe);
    return -RT_ERROR;
}
INIT_BOARD_EXPORT(h7_sdram_init);

#endif /* BSP_USING_SDRAM && SOC_SERIES_GD32H7xx */

2.2.2、SDIO驱动

注意sdio开启后会自动选中rt-thread自带的fs功能组件,仅支持fat文件系统格式。 ~/gd32/rt-thread/bsp/gd32/arm/libraries/gd32_drivers/drv_sdio_h7.c

c 复制代码
/*
 * GD32H7xx SDIO host driver for RT-Thread v5.x MMCSD framework
 *
 * 设计说明:
 *   只实现 rt_mmcsd_host_ops 三件事:request / set_iocfg / get_card_status。
 *   卡片识别、CSD 解析、块设备注册交给 components/drivers/sdio 的 mmcsd
 *   框架,DFS/elm-FatFS 挂载框架生成的 "sd0"。
 *
 *   H7 的 SDIO 是新 IP(多实例、IDMA、trans_start/stop),本驱动以官方
 *   GD32H7xx Demo Suites 18_SDIO_SDCardTest 的 sdcard.c 为协议层蓝本。
 *   数据通路支持 IDMA(默认)与 FIFO polling 双模式,SDIO_USE_IDMA 切换;
 *   IDMA 由 SDIO 内置 DMA 直搬内存,不占 DMA 通道。缓冲区不满足
 *   8 字节对齐 / 32 字节整数倍 / <=8160 字节时自动回退 polling。
 *
 * 移植指南:
 *   1. 引脚:本板 CLK=PC12 CMD=PD2 D0=PB13 D1~D3=PC9/PC10/PC11,全部 AF12
 *      注意 D0 经过跳线 JP65,必须拨 (2,3)=SDIO;(1,2) 是 CAN1_TX
 *   2. 时钟:SDIOCLK 默认取自 PLL1R=200MHz(官方 demo 配置 25M/5*40)。
 *      若 SystemClock_Config 未使能 PLL1R 输出,需在 board.c 补上:
 *      rcu_pll_input_output_clock_range_config(IDX_PLL1, RCU_PLL1RNG_4M_8M,
 *          RCU_PLL1VCO_192M_836M); rcu_pll1_config(5,40,1,1,1);
 *      rcu_pll_clock_output_enable(RCU_PLL1R); rcu_osci_on(RCU_PLL1_CK);
 *      改动时钟源后同步修改 SDIO_CLK_SRC_HZ。分频为直接分频:
 *      400kHz=500, 25MHz=8, 50MHz=4(驱动按目标频率自动计算)
 *   3. 换用 SDIO1 实例时改 SDIO_PERIPH/SDIO_RCU/时钟配置三处
 *   4. 有卡检测引脚的板子在 get_card_status 里返回真实电平
 */

#include <rthw.h>
#include <rtthread.h>
#include <rtdevice.h>

#if defined(BSP_USING_SDIO)

#include <drivers/dev_mmcsd_core.h>
#include <drivers/mmcsd_host.h>
#include <finsh.h>
#if defined(BSP_SDIO_REPROBE_CMD)
#include <dfs_fs.h>                     /* sd_reprobe 移除前主动 umount */
#endif
#include "gd32h7xx_sdio.h"
#include "drv_gpio.h"

#define DBG_TAG             "drv.sdio"
#define DBG_LVL             DBG_INFO
#include <rtdbg.h>

/* ===================== 配置区 ===================== */

#define SDIO_PERIPH         SDIO0               /* 使用 SDIO0 实例 */
#define SDIO_RCU            RCU_SDIO0

/* 总线宽度由 Kconfig 决定:BSP_SDIO_1BIT=y 时 1-bit(本板开 TLI 必须,
 * PC11=SDIO_D3 与 LCD_B4 经 JP52 物理二选一,拨到 LCD 侧后 D3 不存在,
 * 4-bit 会在 ACMD6 切宽后通信全挂;1-bit 只用 CLK/CMD/D0 与 LCD 零冲突,
 * ~6MB/s@50MHz,CSV 记录足够)。自画板无此冲突时不选即恢复 4-bit */
#if defined(BSP_SDIO_1BIT)
#define SDIO_1BIT_MODE      1
#else
#define SDIO_1BIT_MODE      0
#endif

/* SDIO0 内核时钟源:PLL1R = 25MHz HXTAL /5 *40 /1 = 200MHz
 * 置 1:驱动自行配置 PLL1(board.c 未配置 PLL1 时使用,推荐------补丁自包含)
 * 置 0:系统时钟树已配置 PLL1,驱动不再重复配置 */
#define SDIO_PLL1_SELF_CONFIG   1

/* SDIO 内核时钟源频率(分频为直接分频,非 F4 的 div+2) */
#define SDIO_CLK_SRC_HZ     200000000UL

/* 数据通路选择:1=IDMA(推荐,根治 RXORE/TXURE),0=FIFO polling。
 * H7 的 IDMA 是 SDIO 外设内置 DMA,不占 DMA 通道、不用 DMAMUX。
 * polling 通路在 RTOS 中断抢占下必然溢出:25MHz/4bit=12.5MB/s,
 * FIFO 32 字仅缓冲 ~10us,任何 >2.5us 的抢占即 RXORE(实测
 * bytes=220/512)。IDMA 由硬件直搬内存,与 CPU 抢占无关。 */
#define SDIO_USE_IDMA       1

/* FIFO polling 通路的时钟上限:12MHz(div=16,FIFO 缓冲 ~21us) */
#define SDIO_POLLING_MAX_HZ 12000000UL

/* IDMA 通路的时钟上限:50MHz(div=4,4bit 理论 25MB/s) */
#define SDIO_IDMA_MAX_HZ    50000000UL

/* IDMA 约束(gd32h7xx_sdio.h 寄存器定义):
 * - IDMASIZE 位宽 BITS(5,12),单位 32 字节 => 单次最大 255*32=8160 字节
 * - IDMAADDR0 要求地址 8 字节对齐
 *
 * 为什么不直接用框架给的 data->buf 做 IDMA:M7 的 D-cache 维护必须按
 * 32B cache line 对齐放大,而 invalidate 会丢弃 line 内的脏数据。
 * 框架 buffer 来自 rt_malloc/栈(8B 对齐),两端与堆元数据/栈变量共享
 * cache line,invalidate 放大的区域会悄悄破坏相邻内存(实测调度路径
 * 崩溃 pc=0 INVSTATE)。因此 IDMA 只操作驱动私有的 bounce buffer,
 * 框架 buffer 用 memcpy 进出------8KB 一次的 memcpy 代价远小于 RXORE 重传。 */
#define SDIO_IDMA_MAX_BYTES (255UL * 32UL)

/* IDMA 专用 bounce buffer:32B 对齐、驱动独占 => cache 维护绝对安全。
 * 请求被 host 锁串行化,一块静态 buffer 足够 */
static rt_uint8_t sdio_idma_bounce[SDIO_IDMA_MAX_BYTES]
    __attribute__((aligned(32)));

/* 命令/数据超时(rt_tick 起始相对式) */
#define SDIO_CMD_TIMEOUT    rt_tick_from_millisecond(200)
#define SDIO_DAT_TIMEOUT    rt_tick_from_millisecond(2000)

/* DSM 数据超时计数(官方 demo 值) */
#define SDIO_DATATIMEOUT    0xFFFF0000UL

/* 标志集合(来自官方 demo,新 IP 位定义与 F4 不同) */
#define SDIO_MASK_CMD_FLAGS     0x002000C5UL    /* 命令通道标志清除掩码 */
#define SDIO_MASK_DATA_FLAGS    0x18000F3AUL    /* 数据通道标志清除掩码 */
#define SDIO_DAT_READ_END       (SDIO_FLAG_DTCRCERR | SDIO_FLAG_DTTMOUT | \
                                 SDIO_FLAG_RXORE | SDIO_FLAG_DTBLKEND | SDIO_FLAG_DTEND)
#define SDIO_DAT_WRITE_END      (SDIO_FLAG_DTCRCERR | SDIO_FLAG_DTTMOUT | \
                                 SDIO_FLAG_TXURE | SDIO_FLAG_DTBLKEND | SDIO_FLAG_DTEND)
#define SDIO_DAT_IDMA_END       (SDIO_FLAG_DTCRCERR | SDIO_FLAG_DTTMOUT | \
                                 SDIO_FLAG_IDMAERR | SDIO_FLAG_DTEND)
#define SD_FIFOHALF_WORDS       8               /* FIFO 半满/半空阈值(字) */

/* IDMA 前置条件:总长不超 bounce 容量即可(buffer 对齐由 bounce 兜底) */
static rt_bool_t sdio_idma_capable(const struct rt_mmcsd_data *data)
{
#if SDIO_USE_IDMA
    return (data->blksize * data->blks) <= SDIO_IDMA_MAX_BYTES;
#else
    return RT_FALSE;
#endif
}

/* bounce buffer 的 cache 维护。bounce 本身 32B 对齐且驱动独占,
 * 对齐放大不越界,invalidate/flush 都安全 */
static void sdio_bounce_cache_ops(rt_uint32_t total, rt_bool_t is_read)
{
#ifdef RT_USING_CACHE
    rt_uint32_t size = (total + 0x1FU) & ~0x1FU;

    if (is_read)
        rt_hw_cpu_dcache_ops(RT_HW_CACHE_INVALIDATE, sdio_idma_bounce, size);
    else
        rt_hw_cpu_dcache_ops(RT_HW_CACHE_FLUSH, sdio_idma_bounce, size);
#endif
}

/* ===================== 内部工具 ===================== */

/* 等待 STAT 中任一标志位置位,rt_tick 起始相对式超时 */
static rt_err_t sdio_wait_stat(rt_uint32_t flags, rt_tick_t timeout)
{
    rt_tick_t start = rt_tick_get();

    while ((SDIO_STAT(SDIO_PERIPH) & flags) == 0)
    {
        if (rt_tick_get() - start > timeout)
            return -RT_ETIMEOUT;
    }
    return RT_EOK;
}

/* 块大小编码:512 -> DATACTL_BLKSZ(9) */
static rt_uint32_t sdio_blocksize_encode(rt_uint32_t bytes)
{
    rt_uint32_t exp = 0;
    while (bytes > 1) { bytes >>= 1; exp++; }
    return DATACTL_BLKSZ(exp);
}

/* 数据阶段等待(FIFO 轮询本体,配置在 request 中已完成),前置声明 */
static rt_err_t sdio_transfer_data_wait(struct rt_mmcsd_data *data);

/* ===================== mmcsd host ops ===================== */

static void h7_sdio_request(struct rt_mmcsd_host *host, struct rt_mmcsd_req *req)
{
    struct rt_mmcsd_cmd *cmd = req->cmd;
    rt_uint32_t resp_cfg;
    rt_uint32_t wait_flags;
    rt_bool_t no_crc;
    rt_err_t err = RT_EOK;

    /* 响应类型映射 */
    switch (cmd->flags & RESP_MASK)
    {
    case RESP_NONE:
        resp_cfg   = SDIO_RESPONSETYPE_NO;
        wait_flags = SDIO_FLAG_CMDSEND;         /* 无响应:等命令发出 */
        break;
    case RESP_R2:
        resp_cfg   = SDIO_RESPONSETYPE_LONG;
        wait_flags = SDIO_FLAG_CMDRECV | SDIO_FLAG_CCRCERR | SDIO_FLAG_CMDTMOUT;
        break;
    default:
        resp_cfg   = SDIO_RESPONSETYPE_SHORT;
        wait_flags = SDIO_FLAG_CMDRECV | SDIO_FLAG_CCRCERR | SDIO_FLAG_CMDTMOUT;
        break;
    }
    /* R3/R4 响应按协议无 CRC,硬件不校验,CCRCERR 不作错误 */
    no_crc = ((cmd->flags & RESP_MASK) == RESP_R3 ||
              (cmd->flags & RESP_MASK) == RESP_R4);

    /* 数据通路必须在命令发出前就位(读响应后数据立即到来) */
    if (req->data)
    {
        rt_uint32_t total = req->data->blksize * req->data->blks;
        rt_bool_t is_read = (req->data->flags & DATA_DIR_READ);

        /* IDMA 必须在数据通路使能前就位(官方 demo 顺序:
         * dma_config -> idma_enable -> data_config -> trans_start)。
         * 官方 dma_config 本体只有两行:sdio_idma_set + buffer0_address_set */
        if (sdio_idma_capable(req->data))
        {
            if (!is_read)
            {
                rt_memcpy(sdio_idma_bounce, req->data->buf, total);
                sdio_bounce_cache_ops(total, RT_FALSE); /* 写出前刷到内存 */
            }
            else
            {
                /* 读前 invalidate:丢弃 bounce 区域可能存在的脏行,
                 * 防止传输中途被回写覆盖 IDMA 刚写入的数据 */
                sdio_bounce_cache_ops(total, RT_TRUE);
            }
            sdio_idma_set(SDIO_PERIPH, SDIO_IDMA_SINGLE_BUFFER,
                          (total + 31) >> 5);
            sdio_idma_buffer0_address_set(SDIO_PERIPH,
                                          (rt_uint32_t)sdio_idma_bounce);
            sdio_idma_enable(SDIO_PERIPH);
        }

        sdio_data_config(SDIO_PERIPH, SDIO_DATATIMEOUT, total,
                         sdio_blocksize_encode(req->data->blksize));
        sdio_data_transfer_config(SDIO_PERIPH, SDIO_TRANSMODE_BLOCKCOUNT,
                                  is_read ? SDIO_TRANSDIRECTION_TOSDIO
                                          : SDIO_TRANSDIRECTION_TOCARD);
        sdio_trans_start_enable(SDIO_PERIPH);
    }

    /* 发命令;CMD12 需要 CSM 带停止位(sd_transfer_stop 的做法) */
    sdio_command_response_config(SDIO_PERIPH, cmd->cmd_code, cmd->arg, resp_cfg);
    sdio_wait_type_set(SDIO_PERIPH, SDIO_WAITTYPE_NO);
    if (cmd->cmd_code == STOP_TRANSMISSION)
        sdio_trans_stop_enable(SDIO_PERIPH);
    sdio_csm_enable(SDIO_PERIPH);

    err = sdio_wait_stat(wait_flags, SDIO_CMD_TIMEOUT);

    if (cmd->cmd_code == STOP_TRANSMISSION)
        sdio_trans_stop_disable(SDIO_PERIPH);

    if (err == RT_EOK && resp_cfg != SDIO_RESPONSETYPE_NO)
    {
        if (SDIO_STAT(SDIO_PERIPH) & SDIO_FLAG_CMDTMOUT)
            err = -RT_ETIMEOUT;
        else if (!no_crc && (SDIO_STAT(SDIO_PERIPH) & SDIO_FLAG_CCRCERR))
            err = -RT_EIO;
    }
    sdio_flag_clear(SDIO_PERIPH, SDIO_MASK_CMD_FLAGS);

    if (err != RT_EOK)
    {
        /* CMD5(IO_SEND_OP_COND) 是框架探测 SD IO 功能卡(WiFi 类)的
         * 标准步骤,纯存储卡按协议必须不响应------超时属正常流程,
         * 只记调试日志,避免误导;其余命令失败才是真错误 */
        if (cmd->cmd_code == 5)
            LOG_D("cmd5 (sdio io probe) no response, not an io card");
        else
            LOG_E("cmd%d arg=0x%x err=%d STAT=0x%x", cmd->cmd_code, cmd->arg,
                  err, SDIO_STAT(SDIO_PERIPH));
        cmd->err = err;
        if (req->data)
        {
            sdio_trans_start_disable(SDIO_PERIPH);
            sdio_idma_disable(SDIO_PERIPH);
            req->data->err = err;
        }
        mmcsd_req_complete(host);   /* 唤醒 mmcsd_send_request 的 sem_ack */
        return;
    }

    /* 取响应 */
    if (resp_cfg == SDIO_RESPONSETYPE_SHORT)
    {
        cmd->resp[0] = sdio_response_get(SDIO_PERIPH, SDIO_RESPONSE0);
    }
    else if (resp_cfg == SDIO_RESPONSETYPE_LONG)
    {
        /* R2:128 位。硬件 RESPONSE0 为最高字(官方 demo csd[0]=RESPONSE0);
         * 框架 GET_BITS 约定 resp[0] 放最高字(off = 3 - bit/32)。
         * 两边都是直序,切勿倒序------倒序会把 CSD_STRUCTURE 读错,
         * 32GB SDHC 卡被按 CSD v1 公式误算成 1.88GB(实测踩坑) */
        cmd->resp[0] = sdio_response_get(SDIO_PERIPH, SDIO_RESPONSE0);
        cmd->resp[1] = sdio_response_get(SDIO_PERIPH, SDIO_RESPONSE1);
        cmd->resp[2] = sdio_response_get(SDIO_PERIPH, SDIO_RESPONSE2);
        cmd->resp[3] = sdio_response_get(SDIO_PERIPH, SDIO_RESPONSE3);
    }
    cmd->err = RT_EOK;

    /* R1B 带忙等待(CMD7 选中/擦除类命令),给卡片一点时间退出 busy */
    if ((cmd->flags & RESP_MASK) == RESP_R1B)
        rt_thread_mdelay(2);

    /* 数据阶段:配置已在命令前完成,这里只做 FIFO 轮询 */
    if (req->data)
        req->data->err = sdio_transfer_data_wait(req->data);

    /* 多块传输框架会附带 stop 命令(CMD12),数据结束后发出 */
    if (req->stop)
    {
        struct rt_mmcsd_cmd *stop = req->stop;

        sdio_flag_clear(SDIO_PERIPH, SDIO_MASK_CMD_FLAGS);
        sdio_command_response_config(SDIO_PERIPH, stop->cmd_code, stop->arg,
                                     SDIO_RESPONSETYPE_SHORT);
        sdio_wait_type_set(SDIO_PERIPH, SDIO_WAITTYPE_NO);
        sdio_trans_stop_enable(SDIO_PERIPH);
        sdio_csm_enable(SDIO_PERIPH);
        stop->err = sdio_wait_stat(SDIO_FLAG_CMDRECV | SDIO_FLAG_CCRCERR |
                                   SDIO_FLAG_CMDTMOUT, SDIO_CMD_TIMEOUT);
        sdio_trans_stop_disable(SDIO_PERIPH);
        sdio_flag_clear(SDIO_PERIPH, SDIO_MASK_CMD_FLAGS);
        if (stop->err == RT_EOK)
            stop->resp[0] = sdio_response_get(SDIO_PERIPH, SDIO_RESPONSE0);
    }

    /* request 完成,释放 sem_ack 唤醒框架(mmcsd_send_request 在等它) */
    mmcsd_req_complete(host);
}

/* 数据配置已在 request 中完成的等待部分 */
static rt_err_t sdio_transfer_data_wait(struct rt_mmcsd_data *data)
{
    rt_uint32_t total = data->blksize * data->blks;
    rt_uint8_t *buf = (rt_uint8_t *)data->buf;
    rt_bool_t is_read = (data->flags & DATA_DIR_READ);
    rt_bool_t use_idma = sdio_idma_capable(data);
    rt_uint32_t end_flags = use_idma ? SDIO_DAT_IDMA_END :
                            (is_read ? SDIO_DAT_READ_END : SDIO_DAT_WRITE_END);
    rt_uint32_t transferred = 0;
    rt_tick_t start = rt_tick_get();
    rt_err_t err = RT_EOK;

    while ((SDIO_STAT(SDIO_PERIPH) & end_flags) == 0)
    {
        if (use_idma)
        {
            /* IDMA 由硬件直搬内存,CPU 只需等结束标志------
             * 与中断抢占彻底解耦,这就是必须上 DMA 的原因 */
            rt_thread_mdelay(1);
        }
        else if (is_read)
        {
            /* RTOS 上下文 polling 策略:FIFO 有货就搬(!RFE),让 FIFO
             * 始终保持接近空,把延迟容忍度用满整个 FIFO 深度。
             * 不能等 RFH 半满才搬------25MHz/4bit 下 8 字只缓冲 ~2.5us,
             * 任意中断抢占即 RXORE(实测 bytes=220/512 溢出) */
            while (!sdio_flag_get(SDIO_PERIPH, SDIO_FLAG_RFE) &&
                   transferred < total)
            {
                rt_uint32_t w = sdio_data_read(SDIO_PERIPH);
                rt_memcpy(buf + transferred, &w, 4);
                transferred += 4;
            }
        }
        else
        {
            if (sdio_flag_get(SDIO_PERIPH, SDIO_FLAG_TFH) && transferred < total)
            {
                rt_int32_t i;
                for (i = 0; i < SD_FIFOHALF_WORDS && transferred < total; i++)
                {
                    rt_uint32_t w = 0;
                    rt_memcpy(&w, buf + transferred, 4);
                    sdio_data_write(SDIO_PERIPH, w);
                    transferred += 4;
                }
            }
        }
        if (rt_tick_get() - start > SDIO_DAT_TIMEOUT)
        {
            err = -RT_ETIMEOUT;
            break;
        }
    }

    if (err == RT_EOK)
    {
        if (SDIO_STAT(SDIO_PERIPH) & SDIO_FLAG_DTCRCERR)
        {
            LOG_E("DTCRCERR (data crc) bytes=%d/%d STAT=0x%x",
                  transferred, total, SDIO_STAT(SDIO_PERIPH));
            err = -RT_EIO;
        }
        else if (SDIO_STAT(SDIO_PERIPH) & SDIO_FLAG_DTTMOUT)
        {
            LOG_E("DTTMOUT (data timeout) bytes=%d/%d STAT=0x%x",
                  transferred, total, SDIO_STAT(SDIO_PERIPH));
            err = -RT_ETIMEOUT;
        }
        else if (SDIO_STAT(SDIO_PERIPH) & SDIO_FLAG_RXORE)
        {
            LOG_E("RXORE (fifo overrun: polling too slow) bytes=%d/%d STAT=0x%x",
                  transferred, total, SDIO_STAT(SDIO_PERIPH));
            err = -RT_EIO;
        }
        else if (SDIO_STAT(SDIO_PERIPH) & SDIO_FLAG_TXURE)
        {
            LOG_E("TXURE (fifo underrun: polling too slow) bytes=%d/%d STAT=0x%x",
                  transferred, total, SDIO_STAT(SDIO_PERIPH));
            err = -RT_EIO;
        }
        else if (SDIO_STAT(SDIO_PERIPH) & SDIO_FLAG_IDMAERR)
        {
            LOG_E("IDMAERR (idma error) STAT=0x%x", SDIO_STAT(SDIO_PERIPH));
            err = -RT_EIO;
        }
    }

    /* 读完成后再次 invalidate:丢弃传输期间被推测/预取进 cache
     * 的旧行,然后把真实数据从 bounce 拷回框架 buffer */
    if (use_idma && is_read && err == RT_EOK)
    {
        sdio_bounce_cache_ops(total, RT_TRUE);
        rt_memcpy(data->buf, sdio_idma_bounce, total);
    }

    if (!use_idma && is_read && err == RT_EOK)
    {
        while (!sdio_flag_get(SDIO_PERIPH, SDIO_FLAG_RFE) &&
               sdio_flag_get(SDIO_PERIPH, SDIO_FLAG_DATSTA) &&
               transferred < total)
        {
            rt_uint32_t w = sdio_data_read(SDIO_PERIPH);
            rt_memcpy(buf + transferred, &w, 4);
            transferred += 4;
        }
    }

    sdio_trans_start_disable(SDIO_PERIPH);
    if (use_idma)
        sdio_idma_disable(SDIO_PERIPH);
    sdio_flag_clear(SDIO_PERIPH, SDIO_MASK_DATA_FLAGS);
    return err;
}

static void h7_sdio_set_iocfg(struct rt_mmcsd_host *host, struct rt_mmcsd_io_cfg *io_cfg)
{
    static rt_uint32_t cur_clock = 0, cur_width = 0;

    /* 时钟:clock=0 表示关钟下电 */
    if (io_cfg->clock != cur_clock)
    {
        if (io_cfg->clock == 0)
        {
            sdio_hardware_clock_disable(SDIO_PERIPH);
            sdio_power_state_set(SDIO_PERIPH, SDIO_POWER_OFF);
        }
        else
        {
            rt_uint32_t target = io_cfg->clock;
            rt_uint32_t div;

            /* 时钟上限按数据通路区分:
             * IDMA    -> 50MHz(硬件搬数据,总线速率与 CPU 无关)
             * polling -> 12MHz(FIFO 缓冲 ~21us,再高抢占即 RXORE/TXURE) */
#if SDIO_USE_IDMA
            if (target > SDIO_IDMA_MAX_HZ)
                target = SDIO_IDMA_MAX_HZ;
#else
            if (target > SDIO_POLLING_MAX_HZ)
                target = SDIO_POLLING_MAX_HZ;
#endif
            div = SDIO_CLK_SRC_HZ / target;     /* 新 IP 直接分频 */
            if (div < 1) div = 1;

            sdio_clock_config(SDIO_PERIPH, SDIO_SDIOCLKEDGE_RISING,
                              SDIO_CLOCKPWRSAVE_DISABLE, div);
            sdio_power_state_set(SDIO_PERIPH, SDIO_POWER_ON);
            sdio_hardware_clock_disable(SDIO_PERIPH);   /* polling 模式关硬件流控 */
            LOG_I("sdio clock %d Hz (div=%d)%s", target, div,
                  target != io_cfg->clock ? " [clamped]" : "");
        }
        cur_clock = io_cfg->clock;
    }

    /* 总线宽度 */
    if (io_cfg->bus_width != cur_width)
    {
        sdio_bus_mode_set(SDIO_PERIPH,
                          io_cfg->bus_width == MMCSD_BUS_WIDTH_4 ?
                          SDIO_BUSMODE_4BIT : SDIO_BUSMODE_1BIT);
        cur_width = io_cfg->bus_width;
        LOG_I("sdio bus %s", cur_width == MMCSD_BUS_WIDTH_4 ? "4bit" : "1bit");
    }
}

static rt_int32_t h7_sdio_get_card_status(struct rt_mmcsd_host *host)
{
    /* GD32H759I-EVAL 卡座无 CD 引脚:始终有卡。有 CD 引脚请返回真实电平 */
    return 1;
}

static void h7_sdio_enable_sdio_irq(struct rt_mmcsd_host *host, rt_int32_t en)
{
    /* 不承载 SDIO 外设(WiFi 等)功能 */
}

static const struct rt_mmcsd_host_ops h7_sdio_ops = {
    .request         = h7_sdio_request,
    .set_iocfg       = h7_sdio_set_iocfg,
    .get_card_status = h7_sdio_get_card_status,
    .enable_sdio_irq = h7_sdio_enable_sdio_irq,
};

#if defined(BSP_SDIO_REPROBE_CMD)
static struct rt_mmcsd_host *h7_sdio_host = RT_NULL;

/* 手动重新探测------仅供"卡座 CD 信号未接 MCU"的板子使用(如本评估板,
 * 原理图卡座仅 CLK/CMD/D0~D3 六线,无 CD 走线,框架永远收不到插拔
 * 事件)。自画板若将 CD 脚接入 MCU,正确机制是:GPIO 外部中断 +
 * 消抖后调用 mmcsd_change(),且 get_card_status() 返回真实电平
 * ------本驱动未实现该机制,届时应关闭 BSP_SDIO_REPROBE_CMD。
 *
 * mmcsd_change 是乒乓语义:card 非空调用走移除分支,为空调用走识别
 * 分支,故重探测需调用两次。
 *
 * 顺序铁律:必须先 umount 再移除------若文件系统还挂着,sd0/sd0p0 的
 * 引用计数非零,旧设备注销失败变成僵尸;重新识别时同名分区注册
 * 冲突(Put partition ... error),整轮 init 失败。因此本命令在
 * 移除前主动 umount /,用户拔卡重插后只需敲 sdprobe 一条命令。
 * 注意:有文件句柄打开时 umount 会失败,需先关掉再用本命令 */
static void sd_reprobe(int argc, char **argv)
{
    if (h7_sdio_host == RT_NULL)
    {
        rt_kprintf("sdio host not initialized\n");
        return;
    }
    if (h7_sdio_host->card != RT_NULL)
    {
        if (dfs_unmount("/") == 0)
            rt_kprintf("unmount / ok\n");
        mmcsd_change(h7_sdio_host);     /* 移除旧 card 与 sd0 块设备 */
        rt_thread_mdelay(200);          /* 等 mmcsd_detect 线程处理完 */
    }
    mmcsd_change(h7_sdio_host);         /* 完整重新识别 + 注册 sd0 */
}
MSH_CMD_EXPORT_ALIAS(sd_reprobe, sdprobe, re-detect sd card after reinsert);
#endif /* BSP_SDIO_REPROBE_CMD */

/* ===================== 初始化与注册 ===================== */

static void h7_sdio_gpio_init(void)
{
    /* 官方 gpio_config 基础上按总线宽度裁剪:全 AF12,100_220MHZ 驱动,
     * CLK 无上下拉,CMD/D0~D3 上拉(板载另有 10K 上拉)。
     * 1-bit 模式只配 CLK/CMD/D0------D3=PC11 已让给 LCD_B4,碰都不能碰;
     * D1/D2(PC9/PC10) 虽空闲,一并跳过保持引脚干净 */
    rcu_periph_clock_enable(RCU_GPIOB);
    rcu_periph_clock_enable(RCU_GPIOC);
    rcu_periph_clock_enable(RCU_GPIOD);

    gpio_af_set(GPIOB, GPIO_AF_12, GPIO_PIN_13);                            /* D0 */
    gpio_af_set(GPIOC, GPIO_AF_12, GPIO_PIN_12);                            /* CLK */
#if !SDIO_1BIT_MODE
    gpio_af_set(GPIOC, GPIO_AF_12, GPIO_PIN_9 | GPIO_PIN_10 |
                       GPIO_PIN_11);                                        /* D1~D3 */
#endif
    gpio_af_set(GPIOD, GPIO_AF_12, GPIO_PIN_2);                             /* CMD */

    gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_13);
    gpio_mode_set(GPIOC, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_12);
#if !SDIO_1BIT_MODE
    gpio_mode_set(GPIOC, GPIO_MODE_AF, GPIO_PUPD_PULLUP,
                  GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11);
#endif
    gpio_mode_set(GPIOD, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_2);

    gpio_output_options_set(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_100_220MHZ, GPIO_PIN_13);
    gpio_output_options_set(GPIOC, GPIO_OTYPE_PP, GPIO_OSPEED_100_220MHZ, GPIO_PIN_12);
#if !SDIO_1BIT_MODE
    gpio_output_options_set(GPIOC, GPIO_OTYPE_PP, GPIO_OSPEED_100_220MHZ,
                            GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11);
#endif
    gpio_output_options_set(GPIOD, GPIO_OTYPE_PP, GPIO_OSPEED_100_220MHZ, GPIO_PIN_2);
}

int rt_hw_sdio_init(void)
{
    struct rt_mmcsd_host *host;

#if SDIO_PLL1_SELF_CONFIG
    /* PLL1R = 25MHz HXTAL /5 *40 /1 = 200MHz,供 SDIO0 内核时钟。
     * 症状验证:若此处未配置,所有命令超时且 SDIO_STAT=0x1000(CMDSTA
     * 命令状态机挂死在激活态------内核时钟缺失的铁证) */
    rcu_pll_input_output_clock_range_config(IDX_PLL1, RCU_PLL1RNG_4M_8M,
                                            RCU_PLL1VCO_192M_836M);
    rcu_pll1_config(5U, 40U, 1U, 1U, 1U);     /* ndiv=5, n=40, p/q/r=1 */
    rcu_pll_clock_output_enable(RCU_PLL1R);
    rcu_osci_on(RCU_PLL1_CK);
    if (ERROR == rcu_osci_stab_wait(RCU_PLL1_CK))
    {
        LOG_E("PLL1 lock failed");
        return -RT_ERROR;
    }
#endif

    /* SDIO 时钟源:PLL1R=200MHz */
    rcu_sdio_clock_config(IDX_SDIO0, RCU_SDIO0SRC_PLL1R);
    rcu_periph_clock_enable(SDIO_RCU);
    h7_sdio_gpio_init();
    sdio_deinit(SDIO_PERIPH);

    host = mmcsd_alloc_host();
    if (!host)
    {
        LOG_E("alloc host failed");
        return -RT_ENOMEM;
    }

    host->ops        = &h7_sdio_ops;
    host->freq_min   = 400000;                  /* 识别阶段 400kHz,div=500 */
#if SDIO_USE_IDMA
    host->freq_max   = SDIO_IDMA_MAX_HZ;        /* IDMA 上限 50MHz */
#else
    host->freq_max   = SDIO_POLLING_MAX_HZ;     /* polling 上限 12MHz(见 set_iocfg 钳位) */
#endif
    host->valid_ocr  = VDD_32_33 | VDD_33_34;
#if SDIO_1BIT_MODE
    /* 不声明 BUSWIDTH_4:框架不会发 ACMD6 切宽,全程 1-bit */
    host->flags      = MMCSD_MUTBLKWRITE;
#else
    host->flags      = MMCSD_BUSWIDTH_4 | MMCSD_MUTBLKWRITE;
#endif
    host->max_seg_size    = 2048;
    host->max_dma_segs    = 1;
    host->max_blk_size    = 512;
    /* 单次传输必须装进 bounce buffer:8 块 * 512 = 4KB < 8160B 上限。
     * 更大的多擦/多写由框架拆成多次 request(读性能损失可忽略) */
    host->max_blk_count   = 8;

#if defined(BSP_SDIO_REPROBE_CMD)
    h7_sdio_host = host;
#endif
    mmcsd_change(host);

    LOG_I("sdio0 host registered");
    return RT_EOK;
}
INIT_DEVICE_EXPORT(rt_hw_sdio_init);

#endif /* BSP_USING_SDIO */

2.2.3、TLI驱动

c 复制代码
/*
 * GD32H7xx TLI LCD driver (480x272 RGB panel) for RT-Thread
 *
 * 设计说明:
 *   TLI 是"自带 DMA 的显示控制器":初始化后硬件按像素钟持续从帧缓冲
 *   搬数据到面板,CPU 只需往帧缓冲写像素,无需周期性维护。
 *   本驱动以官方 GD32H7xx Demo Suites 24_TLI_IPA 的 tli_config /
 *   tli_gpio_config 为蓝本,改动两处:
 *     1. 帧缓冲从内部 SRAM(0x24005000) 移到 SDRAM(0xC0000000)------
 *        480*272*2=255KB,且 SDRAM 已被 drv_sdram_h7 配成 MPU
 *        non-cacheable,TLI 的 DMA 读取与 CPU 写入天然一致,
 *        不需要 cache 维护(对比 SDIO 的 bounce buffer,两种解法);
 *     2. demo 的 while(1) 死等换成 LOG_E + return。
 *
 * 板级冲突(博客必读,原理图实锤):
 *   - PC11 = LCD_B4 / SDIO0_D3,JP52 物理二选一:TLI 需拨 (1,2);
 *     此时 SDIO 只能跑 1-bit(CLK/CMD/D0 不与 LCD 冲突)
 *   - PG11/PG13/PG14 = LCD_B3/背光/LCD_B0 撞 ENET RMII:开 TLI 必须关 ENET
 *   - PA5 = LCD_R4 撞 DAC0_OUT1:波形显示用 ADC(PC2_C 独占),DAC 让位
 *
 * 移植指南:
 *   1. Kconfig 打开 BSP_USING_TLI(select BSP_USING_SDRAM,关 BSP_USING_ENET)
 *   2. 跳线:JP52=(1,2);确认 ENET 相关跳线不挡 LCD 信号
 *   3. 时序参数在配置区,换面板只改时序宏与时钟分频
 */

#include <rthw.h>
#include <rtthread.h>

#if defined(BSP_USING_TLI)

#include "gd32h7xx_tli.h"
#include "drv_gpio.h"
#include <finsh.h>

#define DBG_TAG             "drv.tli"
#define DBG_LVL             DBG_INFO
#include <rtdbg.h>

/* ===================== 配置区 ===================== */

/* 面板时序(官方 24_TLI_IPA demo 值,全低有效极性) */
#define LCD_HSW             41          /* 水平同步脉宽 */
#define LCD_HBP             2           /* 水平后沿 */
#define LCD_HFP             2           /* 水平前沿 */
#define LCD_VSW             10          /* 垂直同步脉宽 */
#define LCD_VBP             2           /* 垂直后沿 */
#define LCD_VFP             2           /* 垂直前沿 */

#define LCD_WIDTH           480
#define LCD_HEIGHT          272
#define LCD_BPP             2           /* RGB565 = 2 字节/像素 */

/* 像素钟:PLL2R = 25MHz HXTAL /25 *192 /3 = 64MHz,再 8 分频 = 8MHz。
 * 帧率 = 8MHz / ((41+2+480+2) * (10+2+272+2)) ≈ 53Hz */
#define TLI_PLL2_SELF_CONFIG    1

/* 帧缓冲地址:SDRAM 起点(drv_sdram_h7 已配 MPU non-cacheable)。
 * 排障分段:若黑屏,改 0x24005000(官方 demo 验证过的内部 SRAM)
 * 可二分定位------SRAM 能显示 = SDRAM 通路问题;SRAM 也黑 = 时钟/配置问题 */
#define TLI_FB_ADDR         0xC0000000UL

/* ===================== 对外接口 ===================== */

static volatile rt_uint16_t *const tli_fb = (volatile rt_uint16_t *)TLI_FB_ADDR;

/* 写像素(越界丢弃)。面板 RGB888 走线 + 层 RGB565 + 硬件 dither 补位 */
void tli_draw_point(rt_uint16_t x, rt_uint16_t y, rt_uint16_t color)
{
    if (x < LCD_WIDTH && y < LCD_HEIGHT)
        tli_fb[y * LCD_WIDTH + x] = color;
}

/* 整屏填充 */
void tli_fill(rt_uint16_t color)
{
    rt_uint32_t i, total = LCD_WIDTH * LCD_HEIGHT;
    for (i = 0; i < total; i++)
        tli_fb[i] = color;
}

/* 水平线(波形显示的基本件) */
void tli_draw_hline(rt_uint16_t x, rt_uint16_t y, rt_uint16_t len,
                    rt_uint16_t color)
{
    while (len--)
        tli_draw_point(x++, y, color);
}

/* 垂直线 */
void tli_draw_vline(rt_uint16_t x, rt_uint16_t y, rt_uint16_t len,
                    rt_uint16_t color)
{
    while (len--)
        tli_draw_point(x, y++, color);
}

/* ===================== 硬件初始化 ===================== */

static void tli_gpio_config(void)
{
    /* 官方 gpio_config 原样移植:数据/同步线全 AF 推挽 85MHz,
     * PB5 例外 AF3,背光 PG13 普通推挽输出 */
    rcu_periph_clock_enable(RCU_GPIOA);
    rcu_periph_clock_enable(RCU_GPIOB);
    rcu_periph_clock_enable(RCU_GPIOC);
    rcu_periph_clock_enable(RCU_GPIOD);
    rcu_periph_clock_enable(RCU_GPIOE);
    rcu_periph_clock_enable(RCU_GPIOF);
    rcu_periph_clock_enable(RCU_GPIOG);
    rcu_periph_clock_enable(RCU_GPIOH);

    /* HSYNC(PE15, AF10) VSYNC(PA7) PCLK(PG7) */
    gpio_af_set(GPIOE, GPIO_AF_10, GPIO_PIN_15);
    gpio_af_set(GPIOA, GPIO_AF_14, GPIO_PIN_7);
    gpio_af_set(GPIOG, GPIO_AF_14, GPIO_PIN_7);
    gpio_mode_set(GPIOE, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_15);
    gpio_output_options_set(GPIOE, GPIO_OTYPE_PP, GPIO_OSPEED_85MHZ, GPIO_PIN_15);
    gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_7);
    gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_85MHZ, GPIO_PIN_7);
    gpio_mode_set(GPIOG, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_7);
    gpio_output_options_set(GPIOG, GPIO_OTYPE_PP, GPIO_OSPEED_85MHZ, GPIO_PIN_7);

    /* R7(PG6) R6(PH12) R5(PH11) R4(PA5) R3(PH9) R2(PH8) R1(PH3) R0(PH2) */
    gpio_af_set(GPIOG, GPIO_AF_14, GPIO_PIN_6);
    gpio_af_set(GPIOH, GPIO_AF_14,
                GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_8 | GPIO_PIN_9 |
                GPIO_PIN_11 | GPIO_PIN_12);
    gpio_af_set(GPIOA, GPIO_AF_14, GPIO_PIN_5);
    gpio_mode_set(GPIOG, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_6);
    gpio_output_options_set(GPIOG, GPIO_OTYPE_PP, GPIO_OSPEED_85MHZ, GPIO_PIN_6);
    gpio_mode_set(GPIOH, GPIO_MODE_AF, GPIO_PUPD_NONE,
                  GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_8 | GPIO_PIN_9 |
                  GPIO_PIN_11 | GPIO_PIN_12);
    gpio_output_options_set(GPIOH, GPIO_OTYPE_PP, GPIO_OSPEED_85MHZ,
                            GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_8 | GPIO_PIN_9 |
                            GPIO_PIN_11 | GPIO_PIN_12);
    gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_5);
    gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_85MHZ, GPIO_PIN_5);

    /* G7(PD3) G6(PC7) G5(PC1) G4(PH15) G3(PH14) G2(PH13) G1(PB0) G0(PB1) */
    gpio_af_set(GPIOD, GPIO_AF_14, GPIO_PIN_3);
    gpio_af_set(GPIOC, GPIO_AF_14, GPIO_PIN_7 | GPIO_PIN_1);
    gpio_af_set(GPIOH, GPIO_AF_14,
                GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15);
    gpio_af_set(GPIOB, GPIO_AF_14, GPIO_PIN_0 | GPIO_PIN_1);
    gpio_mode_set(GPIOD, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_3);
    gpio_output_options_set(GPIOD, GPIO_OTYPE_PP, GPIO_OSPEED_85MHZ, GPIO_PIN_3);
    gpio_mode_set(GPIOC, GPIO_MODE_AF, GPIO_PUPD_NONE,
                  GPIO_PIN_7 | GPIO_PIN_1);
    gpio_output_options_set(GPIOC, GPIO_OTYPE_PP, GPIO_OSPEED_85MHZ,
                            GPIO_PIN_7 | GPIO_PIN_1);
    gpio_mode_set(GPIOH, GPIO_MODE_AF, GPIO_PUPD_NONE,
                  GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15);
    gpio_output_options_set(GPIOH, GPIO_OTYPE_PP, GPIO_OSPEED_85MHZ,
                            GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15);
    gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_0 | GPIO_PIN_1);
    gpio_output_options_set(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_85MHZ,
                            GPIO_PIN_0 | GPIO_PIN_1);

    /* B7(PB9) B6(PB8) B5(PB5,AF3) B4(PC11) B3(PG11) B2(PG10) B1(PG12) B0(PG14) */
    gpio_af_set(GPIOB, GPIO_AF_14, GPIO_PIN_9 | GPIO_PIN_8);
    gpio_af_set(GPIOB, GPIO_AF_3, GPIO_PIN_5);
    gpio_af_set(GPIOC, GPIO_AF_14, GPIO_PIN_11);
    gpio_af_set(GPIOG, GPIO_AF_14,
                GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_14);
    gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_9 | GPIO_PIN_8);
    gpio_output_options_set(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_85MHZ,
                            GPIO_PIN_9 | GPIO_PIN_8);
    gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_5);
    gpio_output_options_set(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_85MHZ, GPIO_PIN_5);
    gpio_mode_set(GPIOC, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_11);
    gpio_output_options_set(GPIOC, GPIO_OTYPE_PP, GPIO_OSPEED_85MHZ, GPIO_PIN_11);
    gpio_mode_set(GPIOG, GPIO_MODE_AF, GPIO_PUPD_NONE,
                  GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_14);
    gpio_output_options_set(GPIOG, GPIO_OTYPE_PP, GPIO_OSPEED_85MHZ,
                            GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_14);

    /* DE(PF10) */
    gpio_af_set(GPIOF, GPIO_AF_14, GPIO_PIN_10);
    gpio_mode_set(GPIOF, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_10);
    gpio_output_options_set(GPIOF, GPIO_OTYPE_PP, GPIO_OSPEED_85MHZ, GPIO_PIN_10);

    /* 背光 PG13:普通推挽输出,置高点亮(demo 用 GPIO 非 PWM) */
    gpio_mode_set(GPIOG, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_PIN_13);
    gpio_output_options_set(GPIOG, GPIO_OTYPE_PP, GPIO_OSPEED_85MHZ, GPIO_PIN_13);
    gpio_bit_set(GPIOG, GPIO_PIN_13);
}

static rt_err_t tli_hw_config(void)
{
    tli_parameter_struct tli_init_struct;
    tli_layer_parameter_struct layer_init_struct;

    rcu_periph_clock_enable(RCU_TLI);

#if TLI_PLL2_SELF_CONFIG
    /* PLL2R = 25MHz HXTAL /25 *192 /3 = 64MHz,DIV8 后像素钟 8MHz。
     * 症状验证:PLL2 未配置时 TLI 无像素钟,面板全黑且无同步信号 */
    rcu_pll_input_output_clock_range_config(IDX_PLL2, RCU_PLL2RNG_1M_2M,
                                            RCU_PLL2VCO_192M_836M);
    if (ERROR == rcu_pll2_config(25U, 192U, 3U, 3U, 3U))
    {
        LOG_E("PLL2 config failed");
        return -RT_ERROR;
    }
    rcu_pll_clock_output_enable(RCU_PLL2R);
    rcu_tli_clock_div_config(RCU_PLL2R_DIV8);
    rcu_osci_on(RCU_PLL2_CK);
    if (ERROR == rcu_osci_stab_wait(RCU_PLL2_CK))
    {
        LOG_E("PLL2 lock failed");
        return -RT_ERROR;
    }
#endif

    /* 时序:极性全低有效,寄存器值均为实际值-1(官方 demo 原样) */
    tli_init_struct.signalpolarity_hs      = TLI_HSYN_ACTLIVE_LOW;
    tli_init_struct.signalpolarity_vs      = TLI_VSYN_ACTLIVE_LOW;
    tli_init_struct.signalpolarity_de      = TLI_DE_ACTLIVE_LOW;
    tli_init_struct.signalpolarity_pixelck = TLI_PIXEL_CLOCK_TLI;
    tli_init_struct.synpsz_hpsz   = LCD_HSW - 1;
    tli_init_struct.synpsz_vpsz   = LCD_VSW - 1;
    tli_init_struct.backpsz_hbpsz = LCD_HSW + LCD_HBP - 1;
    tli_init_struct.backpsz_vbpsz = LCD_VSW + LCD_VBP - 1;
    tli_init_struct.activesz_hasz = LCD_HSW + LCD_HBP + LCD_WIDTH - 1;
    tli_init_struct.activesz_vasz = LCD_VSW + LCD_VBP + LCD_HEIGHT - 1;
    tli_init_struct.totalsz_htsz  = LCD_HSW + LCD_HBP + LCD_WIDTH + LCD_HFP - 1;
    tli_init_struct.totalsz_vtsz  = LCD_VSW + LCD_VBP + LCD_HEIGHT + LCD_VFP - 1;
    tli_init_struct.backcolor_red   = 0;
    tli_init_struct.backcolor_green = 0;
    tli_init_struct.backcolor_blue  = 0;
    tli_init(&tli_init_struct);

    /* 层 0:全屏窗口,RGB565,帧缓冲在 SDRAM */
    layer_init_struct.layer_window_leftpos   = LCD_HSW + LCD_HBP;
    layer_init_struct.layer_window_rightpos  = LCD_HSW + LCD_HBP + LCD_WIDTH - 1;
    layer_init_struct.layer_window_toppos    = LCD_VSW + LCD_VBP;
    layer_init_struct.layer_window_bottompos = LCD_VSW + LCD_VBP + LCD_HEIGHT - 1;
    layer_init_struct.layer_ppf = LAYER_PPF_RGB565;
    layer_init_struct.layer_sa  = 255;
    layer_init_struct.layer_default_red   = 0;
    layer_init_struct.layer_default_green = 0;
    layer_init_struct.layer_default_blue  = 0;
    layer_init_struct.layer_default_alpha = 0;
    layer_init_struct.layer_acf1 = LAYER_ACF1_SA;
    layer_init_struct.layer_acf2 = LAYER_ACF2_SA;
    layer_init_struct.layer_frame_bufaddr           = TLI_FB_ADDR;
    layer_init_struct.layer_frame_line_length       = LCD_WIDTH * LCD_BPP + 7;
    layer_init_struct.layer_frame_buf_stride_offset = LCD_WIDTH * LCD_BPP;
    layer_init_struct.layer_frame_total_line_number = LCD_HEIGHT;
    tli_layer_init(LAYER0, &layer_init_struct);

    /* RGB565 -> RGB888 走线的低位靠硬件 dither 补,显著改善色带 */
    tli_dither_config(TLI_DITHER_ENABLE);

    tli_layer_enable(LAYER0);
    tli_reload_config(TLI_REQUEST_RELOAD_EN);
    tli_enable();
    return RT_EOK;
}

int rt_hw_tli_init(void)
{
    tli_gpio_config();      /* 严格按官方 demo 顺序:先 GPIO 后控制器 */
    if (tli_hw_config() != RT_EOK)
        return -RT_ERROR;

    tli_fill(0x0000);       /* 上电黑屏,避免显示 SDRAM 随机数据 */
#if TLI_FB_ADDR != 0xC0000000UL
    rt_hw_cpu_dcache_ops(RT_HW_CACHE_FLUSH, (void *)tli_fb,
                         LCD_WIDTH * LCD_HEIGHT * LCD_BPP);
#endif
    LOG_I("tli lcd %dx%d @ fb=0x%x", LCD_WIDTH, LCD_HEIGHT, TLI_FB_ADDR);
    return RT_EOK;
}
INIT_DEVICE_EXPORT(rt_hw_tli_init);

/* ===================== 测试命令 ===================== */

/* tli_test <bars|red|green|blue|white|black|grid> */
static void tli_test(int argc, char **argv)
{
    const char *pat = (argc >= 2) ? argv[1] : "bars";
    rt_uint16_t x, y;

    if (!rt_strcmp(pat, "bars"))
    {
        /* 八色竖条:验证数据线每一位------某色通道断线时对应条纹异常 */
        static const rt_uint16_t bars[8] = {
            0xFFFF, 0xFFE0, 0x07E0, 0x07FF, 0xF800, 0xF81F, 0x001F, 0x0000
        };
        for (x = 0; x < LCD_WIDTH; x++)
            for (y = 0; y < LCD_HEIGHT; y++)
                tli_fb[y * LCD_WIDTH + x] = bars[x / (LCD_WIDTH / 8)];
    }
    else if (!rt_strcmp(pat, "grid"))
    {
        /* 网格:验证行列寻址------frame_stride 错时网格歪斜 */
        tli_fill(0x0000);
        for (x = 0; x < LCD_WIDTH; x += 48)
            tli_draw_vline(x, 0, LCD_HEIGHT, 0x07E0);
        for (y = 0; y < LCD_HEIGHT; y += 34)
            tli_draw_hline(0, y, LCD_WIDTH, 0x07E0);
    }
    else
    {
        rt_uint16_t color = 0x0000;
        if      (!rt_strcmp(pat, "red"))   color = 0xF800;
        else if (!rt_strcmp(pat, "green")) color = 0x07E0;
        else if (!rt_strcmp(pat, "blue"))  color = 0x001F;
        else if (!rt_strcmp(pat, "white")) color = 0xFFFF;
        else if (!rt_strcmp(pat, "black")) color = 0x0000;
        else
        {
            rt_kprintf("usage: tli_test <bars|red|green|blue|white|black|grid>\n");
            return;
        }
        tli_fill(color);
    }
#if TLI_FB_ADDR != 0xC0000000UL
    /* 帧缓冲在内部 SRAM 的排障形态:D-cache 开启,写完必须刷到内存
     * TLI 才能看见(官方 demo 在主循环里周期性 CleanInvalidate 同理) */
    rt_hw_cpu_dcache_ops(RT_HW_CACHE_FLUSH, (void *)tli_fb,
                         LCD_WIDTH * LCD_HEIGHT * LCD_BPP);
#endif
    rt_kprintf("tli_test %s done\n", pat);
}
MSH_CMD_EXPORT_ALIAS(tli_test, tli_test,
                     lcd self test: tli_test <bars|red|green|blue|white|black|grid>);

/* tli_dump:无示波器条件下的 TLI 运行状态取证。
 * 判读指南:
 *   CPPOS 两次读值不同     -> 像素钟活着,TLI 正在扫描(时钟域无罪)
 *   CPPOS 恒为 0/不变      -> TLI 停摆,查 PLL2/时钟/使能
 *   INTF bit2(TEF)=1       -> 读帧缓冲总线事务错误,查 FB 地址/SDRAM
 *   INTF bit1(FEF)=1       -> FIFO 错误,数据供应不上(带宽/时钟比)
 *   L0FBADDR               -> 必须与 TLI_FB_ADDR 一致(reload 生效证据) */
static void tli_dump(int argc, char **argv)
{
    rt_uint32_t pos1, pos2;

    pos1 = TLI_CPPOS;
    rt_thread_mdelay(20);
    pos2 = TLI_CPPOS;

    rt_kprintf("TLI_CTL    = 0x%08x (bit0 TLIEN=%d)\n", TLI_CTL, TLI_CTL & 1);
    rt_kprintf("TLI_SPSZ   = 0x%08x  BPSZ = 0x%08x\n", TLI_SPSZ, TLI_BPSZ);
    rt_kprintf("TLI_ASZ    = 0x%08x  TSZ  = 0x%08x\n", TLI_ASZ, TLI_TSZ);
    rt_kprintf("TLI_STAT   = 0x%08x (VDE/HDE/VS/HS live)\n", TLI_STAT);
    rt_kprintf("TLI_INTF   = 0x%08x (bit1 FEF fifo-err, bit2 TEF bus-err)\n",
               TLI_INTF);
    rt_kprintf("TLI_CPPOS  = 0x%08x -> 0x%08x (%s)\n", pos1, pos2,
               pos1 != pos2 ? "SCANNING, pixel clock alive"
                            : "FROZEN, tli not running");
    rt_kprintf("L0CTL      = 0x%08x  L0FBADDR = 0x%08x (expect 0x%08x)\n",
               TLI_LXCTL(LAYER0), TLI_LXFBADDR(LAYER0), TLI_FB_ADDR);
    rt_kprintf("L0FLLEN    = 0x%08x  L0FTLN   = 0x%08x\n",
               TLI_LXFLLEN(LAYER0), TLI_LXFTLN(LAYER0));
}
MSH_CMD_EXPORT_ALIAS(tli_dump, tli_dump, dump tli registers for diagnosis);

#endif /* BSP_USING_TLI */

2.2.4、touch驱动

c 复制代码
/*
 * GD32H759I-EVAL GT911 touch driver (software I2C) for RT-Thread
 *
 * 设计说明:
 *   官方 29_TLI_Touch_Draw demo 用 bit-bang I2C(SCL=PH7/SDA=PF9 开漏,
 *   RST=PF6/INT=PH6),本驱动按同一方案移植为 RT-Thread 版本:
 *   - 复位时序决定 7 位地址:RST 拉高时 INT 保持低 >5ms -> 0xBA/0x28
 *   - 16 位寄存器寻址:状态 0x814E、坐标 0x814F、ID 0x8140
 *   - 开漏输出读引脚不回切输入模式(GD32 输入通路常开,写 1 即释放)
 *   - demo 的 EXTI 宏有误(写 GPIOA/PIN5,实际 INT=PH6),本驱动采用
 *     轮询(10ms),触摸坐标的实时性需求远低于波形刷新,够用且稳
 *
 * 依赖:BSP_USING_TLI(touch_test 在屏上画触点;纯读坐标可单独用)
 */

#include <rthw.h>
#include <rtthread.h>

#if defined(BSP_USING_TOUCH)

#include "gd32h7xx_gpio.h"
#include <finsh.h>
#include <stdlib.h>

#define DBG_TAG             "drv.touch"
#define DBG_LVL             DBG_INFO
#include <rtdbg.h>

/* ===================== 配置区 ===================== */

#define GTP_SCL_PORT        GPIOH
#define GTP_SCL_PIN         GPIO_PIN_7
#define GTP_SDA_PORT        GPIOF
#define GTP_SDA_PIN         GPIO_PIN_9
#define GTP_RST_PORT        GPIOF
#define GTP_RST_PIN         GPIO_PIN_6
#define GTP_INT_PORT        GPIOH
#define GTP_INT_PIN         GPIO_PIN_6

#define GTP_ADDRESS         0xBA        /* 8 位写地址(7 位 0x5D << 1 | R/W) */

#define GT911_ID_REG        0x8140      /* 产品 ID,应读出 "911" */
#define GT911_STATUS_REG    0x814E      /* bit7=有数据,bit3:0=触点数 */
#define GT911_POINT_REG     0x814F      /* 每点 8 字节:XL XH YL YH SL SH */
#define GT911_CFG_START     0x8047      /* 配置表起始地址 */

/* bit-bang 半周期延时:600MHz 下约 2~4us。TLI 的 DMA 常占总线后
 * GPIO 翻转延迟变大、时序余量变窄(实测 ID 第三字节 0x31 误读 0x7f),
 * 放宽到 ~150kHz 换稳定------GT911 对速率毫无要求 */
#define I2C_DELAY_LOOP      400

/* ===================== bit-bang I2C 底层 ===================== */

#define SCL_H()  gpio_bit_set(GTP_SCL_PORT, GTP_SCL_PIN)
#define SCL_L()  gpio_bit_reset(GTP_SCL_PORT, GTP_SCL_PIN)
#define SDA_H()  gpio_bit_set(GTP_SDA_PORT, GTP_SDA_PIN)
#define SDA_L()  gpio_bit_reset(GTP_SDA_PORT, GTP_SDA_PIN)
#define SDA_RD() gpio_input_bit_get(GTP_SDA_PORT, GTP_SDA_PIN)
#define SCL_RD() gpio_input_bit_get(GTP_SCL_PORT, GTP_SCL_PIN)

static void i2c_delay(void)
{
    volatile rt_uint32_t i = I2C_DELAY_LOOP;
    while (i--) __asm volatile ("nop");
}

/* 时钟延展处理:SCL_H 只是主机松手,从机忙时会把 SCL 摁在低电平
 * (GT911 在 TLI DMA 高负载下响应变慢,实测不处理会读出确定性
 * 错字节------ID 第三位 0x31 恒读成 0x7f)。必须等线真的变高 */
static void scl_release_wait(void)
{
    volatile rt_uint32_t timeout = 100000;

    SCL_H();
    while (SCL_RD() == RESET && timeout--)
        __asm volatile ("nop");
}

static void i2c_start(void)
{
    SDA_H(); SCL_H(); i2c_delay();
    SDA_L(); i2c_delay();
    SCL_L(); i2c_delay();
}

static void i2c_stop(void)
{
    SDA_L(); SCL_H(); i2c_delay();
    SDA_H(); i2c_delay();
}

/* 发送一字节并读 ACK,返回 SET=收到 ACK */
static FlagStatus i2c_sendbyte(rt_uint8_t byte)
{
    rt_int32_t i;
    FlagStatus ack;

    for (i = 7; i >= 0; i--)
    {
        if (byte & (1 << i)) SDA_H(); else SDA_L();
        i2c_delay();
        scl_release_wait(); i2c_delay();
        SCL_L();
    }
    SDA_H();                    /* 释放 SDA 给从机拉 ACK */
    i2c_delay();
    scl_release_wait();
    ack = (SDA_RD() == RESET) ? SET : RESET;
    i2c_delay();
    SCL_L();
    return ack;
}

/* 读一字节,ack=SET 发 ACK(还要读),RESET 发 NACK(最后一字节) */
static rt_uint8_t i2c_readbyte(FlagStatus ack)
{
    rt_int32_t i;
    rt_uint8_t byte = 0;

    SDA_H();                    /* 释放总线 */
    for (i = 7; i >= 0; i--)
    {
        scl_release_wait(); i2c_delay();
        if (SDA_RD()) byte |= (1 << i);
        SCL_L(); i2c_delay();
    }
    if (ack == SET) SDA_L(); else SDA_H();
    i2c_delay();
    SCL_H(); i2c_delay();
    SCL_L();
    SDA_H();
    return byte;
}

/* 写 16 位寄存器地址 + 数据 */
static rt_err_t gt911_write_reg(rt_uint16_t reg, const rt_uint8_t *buf,
                                rt_uint8_t len)
{
    rt_uint8_t i;

    i2c_start();
    if (i2c_sendbyte(GTP_ADDRESS) == RESET) goto _fail;
    if (i2c_sendbyte(reg >> 8) == RESET)   goto _fail;
    if (i2c_sendbyte(reg & 0xFF) == RESET) goto _fail;
    for (i = 0; i < len; i++)
        if (i2c_sendbyte(buf[i]) == RESET) goto _fail;
    i2c_stop();
    return RT_EOK;
_fail:
    i2c_stop();
    return -RT_EIO;
}

/* 读 16 位寄存器地址 + 数据 */
static rt_err_t gt911_read_reg(rt_uint16_t reg, rt_uint8_t *buf, rt_uint8_t len)
{
    rt_uint8_t i;

    i2c_start();
    if (i2c_sendbyte(GTP_ADDRESS) == RESET)     goto _fail;
    if (i2c_sendbyte(reg >> 8) == RESET)        goto _fail;
    if (i2c_sendbyte(reg & 0xFF) == RESET)      goto _fail;
    i2c_stop();

    i2c_start();
    if (i2c_sendbyte(GTP_ADDRESS | 0x01) == RESET) goto _fail;
    for (i = 0; i < len; i++)
        buf[i] = i2c_readbyte(i == len - 1 ? RESET : SET);
    i2c_stop();
    return RT_EOK;
_fail:
    i2c_stop();
    return -RT_EIO;
}

/* ===================== GT911 配置表(官方 demo 原值,480x272) ===================== */

static const rt_uint8_t gt911_cfg_params[] = {
0x5A,0xE0,0x01,0x10,0x01,0x01,0x8D,0x00,0x01,0x08,0x28,0x05,
0x55,0x32,0x03,0x05,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x89,0x2A,0x0B,0x17,0x15,0x31,0x0D,0x00,0x00,
0x02,0xB9,0x04,0x2C,0x00,0x00,0x00,0x00,0x00,0x03,0x64,0x32,
0x00,0x00,0x00,0x0F,0x94,0x94,0xC5,0x02,0x07,0x00,0x00,0x04,
0x8D,0x13,0x00,0x5C,0x1E,0x00,0x3C,0x30,0x00,0x29,0x4C,0x00,
0x20,0x78,0x00,0x20,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x02,0x04,0x06,0x08,0x0A,0x0C,0x0E,0x10,
0x12,0x14,0x16,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x24,0x22,
0x21,0x20,0x1F,0x1E,0x1D,0x1C,0x18,0x16,0x12,0x10,0x0F,0x0A,
0x08,0x06,0x04,0x02,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0x21,0x01
};

/* ===================== 对外接口 ===================== */

/* 读取第一点触摸坐标。返回 RT_EOK 且有触摸时 *x/*y 有效。
 * 协议要点:状态报告的 N 个触点必须全部读走再清 0x814E------只读部分
 * 触点芯片不接受清位,状态位挂死导致无触摸也疯狂重报(实测:上电
 * 残留状态 0xFF 报 15 点,只读 1 点清不掉;真触摸一次后触点数变 1
 * 恰好读完才恢复正常)。GT911 最多 5 点,>5 判非法帧丢弃 */
rt_err_t gt911_read_point(rt_uint16_t *x, rt_uint16_t *y)
{
    rt_uint8_t status, buf[40];
    rt_uint8_t clear = 0;
    rt_uint8_t n;

    if (gt911_read_reg(GT911_STATUS_REG, &status, 1) != RT_EOK)
        return -RT_EIO;

    if (status & 0x80)
    {
        n = status & 0x0F;
        if (n >= 1 && n <= 5)
        {
            if (gt911_read_reg(GT911_POINT_REG, buf, 8 * n) != RT_EOK)
                return -RT_EIO;
            /* 每点 8 字节布局:byte0=轨迹 ID,byte1/2=X 低/高,
             * byte3/4=Y 低/高,byte5/6=触摸面积。坐标从 byte1 起,
             * 从 byte0 起算会把 ID 拼进坐标低位(实测错出 x=3328) */
            *x = ((rt_uint16_t)buf[2] << 8) | buf[1];
            *y = ((rt_uint16_t)buf[4] << 8) | buf[3];
        }
        gt911_write_reg(GT911_STATUS_REG, &clear, 1);   /* 全量读完再清 */
        return (n >= 1 && n <= 5) ? RT_EOK : -RT_EEMPTY;
    }
    if (status)
        gt911_write_reg(GT911_STATUS_REG, &clear, 1);
    return -RT_EEMPTY;
}

/* ===================== 初始化 ===================== */

int rt_hw_touch_init(void)
{
    rt_uint8_t id[4];
    rt_uint8_t cfg[sizeof(gt911_cfg_params)];
    rt_uint8_t checksum = 0;
    rt_uint32_t i;

    /* GPIO:SCL/SDA 开漏输出,RST 推挽,INT 先推挽(复位时序用) */
    rcu_periph_clock_enable(RCU_GPIOF);
    rcu_periph_clock_enable(RCU_GPIOH);
    gpio_mode_set(GTP_SCL_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GTP_SCL_PIN);
    gpio_output_options_set(GTP_SCL_PORT, GPIO_OTYPE_OD, GPIO_OSPEED_60MHZ, GTP_SCL_PIN);
    gpio_mode_set(GTP_SDA_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GTP_SDA_PIN);
    gpio_output_options_set(GTP_SDA_PORT, GPIO_OTYPE_OD, GPIO_OSPEED_60MHZ, GTP_SDA_PIN);
    gpio_mode_set(GTP_RST_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLDOWN, GTP_RST_PIN);
    gpio_output_options_set(GTP_RST_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_12MHZ, GTP_RST_PIN);
    gpio_mode_set(GTP_INT_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLDOWN, GTP_INT_PIN);
    gpio_output_options_set(GTP_INT_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_12MHZ, GTP_INT_PIN);
    SCL_H(); SDA_H();

    /* 复位时序:RST 低 1ms(INT 保持低)-> RST 高 >5ms -> 地址定为 0xBA,
     * 之后 INT 回输入态 */
    gpio_bit_reset(GTP_RST_PORT, GTP_RST_PIN);
    gpio_bit_reset(GTP_INT_PORT, GTP_INT_PIN);
    rt_thread_mdelay(1);
    gpio_bit_set(GTP_RST_PORT, GTP_RST_PIN);
    rt_thread_mdelay(6);
    gpio_mode_set(GTP_INT_PORT, GPIO_MODE_INPUT, GPIO_PUPD_NONE, GTP_INT_PIN);

    /* 读 ID 验证 I2C 链路。GT911 复位后到 I2C 可应答需要启动时间,
     * 50ms 实测不够(读出全 0xFF),放到 150ms 且重试间隔 50ms。
     * 判定放宽到前两字节 '9','1';失败只警告不判死------运行时坐标
     * 读取已被证明独立可用,本校验仅为尽早暴露接线故障 */
    rt_thread_mdelay(150);
    {
        rt_int32_t retry = 3;
        rt_bool_t id_ok = RT_FALSE;

        while (retry--)
        {
            if (gt911_read_reg(GT911_ID_REG, id, 4) == RT_EOK &&
                id[0] == '9' && id[1] == '1')
            {
                id_ok = RT_TRUE;
                if (id[2] != '1')
                    LOG_W("gt911 id byte2 read 0x%x (expect 0x31), tolerated",
                          id[2]);
                break;
            }
            rt_thread_mdelay(50);
        }
        if (!id_ok)
        {
            LOG_W("gt911 id read failed (0x%x 0x%x 0x%x), continue anyway",
                  id[0], id[1], id[2]);
        }
    }

    /* 下载配置表(官方 demo 值),补校验和与生效标志 */
    rt_memcpy(cfg, gt911_cfg_params, sizeof(cfg));
    for (i = 0; i < sizeof(cfg) - 2; i++)
        checksum += cfg[i];
    cfg[sizeof(cfg) - 2] = (~checksum) + 1;
    cfg[sizeof(cfg) - 1] = 1;
    if (gt911_write_reg(GT911_CFG_START, cfg, sizeof(cfg)) != RT_EOK)
    {
        /* 出厂默认配置恰好匹配本面板(480x272),下载失败不判死 */
        LOG_W("gt911 config download failed, use factory defaults");
    }
    rt_thread_mdelay(10);

    /* 排干上电残留状态:复位后 0x814E 常为 0xFF 之类的脏值,
     * 读不清会在首次触摸前持续误报 */
    {
        rt_uint8_t retry = 5;
        rt_uint16_t dx, dy;
        while (retry-- && gt911_read_point(&dx, &dy) != -RT_EEMPTY)
            rt_thread_mdelay(5);
    }

    LOG_I("gt911 ready (id=%c%c%c, ver=0x%x)", id[0], id[1], id[2], id[3]);
    return RT_EOK;
}
INIT_DEVICE_EXPORT(rt_hw_touch_init);

/* ===================== 测试命令 ===================== */

#if defined(BSP_USING_TLI)
extern void tli_draw_point(rt_uint16_t x, rt_uint16_t y, rt_uint16_t color);
extern void tli_fill(rt_uint16_t color);
#endif

/* touch_id:运行时再读一次 ID------验证"init 读不到"只是上电时序问题 */
static void touch_id(int argc, char **argv)
{
    rt_uint8_t id[4];

    if (gt911_read_reg(GT911_ID_REG, id, 4) == RT_EOK)
        rt_kprintf("gt911 id: %c%c%c ver=0x%02x\n", id[0], id[1], id[2], id[3]);
    else
        rt_kprintf("gt911 id read failed\n");
}
MSH_CMD_EXPORT_ALIAS(touch_id, touch_id, read gt911 product id at runtime);

/* touch_test [seconds]:打印坐标并在屏上画点(默认 10 秒) */
static void touch_test(int argc, char **argv)
{
    rt_uint32_t rounds = 1000;      /* 10ms * 1000 = 10s */
    rt_uint16_t x, y;

    if (argc >= 2)
        rounds = atoi(argv[1]) * 100;

    rt_kprintf("touch the panel (draw mode)...\n");
#if defined(BSP_USING_TLI)
    tli_fill(0x0000);
#endif
    while (rounds--)
    {
        if (gt911_read_point(&x, &y) == RT_EOK)
        {
            rt_kprintf("x=%d y=%d\n", x, y);
#if defined(BSP_USING_TLI)
            if (x < 480 && y < 272)
            {
                tli_draw_point(x, y, 0xFFE0);
                tli_draw_point(x > 0 ? x - 1 : x, y, 0xFFE0);
                tli_draw_point(x, y > 0 ? y - 1 : y, 0xFFE0);
                tli_draw_point(x < 479 ? x + 1 : x, y, 0xFFE0);
                tli_draw_point(x, y < 271 ? y + 1 : y, 0xFFE0);
            }
#endif
        }
        rt_thread_mdelay(10);
    }
    rt_kprintf("touch_test done\n");
}
MSH_CMD_EXPORT_ALIAS(touch_test, touch_test,
                     read gt911 and draw on lcd: touch_test [seconds]);

#endif /* BSP_USING_TOUCH */

2.3、集成进BSP

2.3.1、kconfig修改

~/gd32/rt-thread/bsp/gd32/arm/gd32h759i-eval/board/Kconfig

plain 复制代码
config BSP_USING_SDRAM
        bool "Enable SDRAM (EXMC DEVICE0, MT48LC16M16A2 32MB)"
        default n
    config BSP_USING_SDIO
        bool "Enable SDIO (TF card)"
        select RT_USING_SDIO
        select RT_USING_DFS
        select RT_USING_DFS_ELMFAT
        select RT_USING_DFS_MNTTABLE
        default n

    config BSP_SDIO_AUTO_MOUNT
        bool "Auto mount sd0 to / (event-driven watcher)"
        default y
        help
            mmcsd 卡片识别在 mmcsd_detect 线程异步完成,DFS mount_table 的
            启动即挂载注定失败。本选项启用事件驱动守护线程:收到框架
            mmcsd_wait_cd_changed() 的插入事件后挂载 sd0 到 /,
            收到移除事件后 umount。
    config BSP_SDIO_1BIT
        bool "Force SDIO 1-bit bus (JP52 shared with LCD)"
        default y
        help
            本板 PC11 是 SDIO_D3 又是 LCD_B4,经 JP52 物理二选一:
            拨到 LCD 侧(开 TLI 必须)后 D3 不存在,4-bit 模式会在
            ACMD6 切宽后通信全挂。1-bit 只用 CLK/CMD/D0 三根线,与
            LCD 零冲突可共存,带宽约 6MB/s@50MHz,CSV 记录足够。
            不开 TLI、或自画板无此复用时,关闭本选项恢复 4-bit。
            
    config BSP_SDIO_REPROBE_CMD
        bool "Enable sdprobe shell cmd (board without CD pin)"
        default y
        help
            仅在卡座 CD(Card Detect)信号未接入 MCU 的板子上需要:
            无 CD 脚则框架永远收不到插拔事件,拔卡重插后需在 shell
            执行 sdprobe 手动重新探测。自画板若把 CD 脚接到 MCU,
            正确做法是 GPIO 外部中断 + 消抖后调用 mmcsd_change(),
            并让 get_card_status() 返回真实电平------本工程未实现该机制,
            届时应关闭本选项。

    config BSP_USING_TLI
        bool "Enable TLI LCD (480x272 RGB panel)"
        default n
        select BSP_USING_SDRAM
        help
            注意板级冲突:开 TLI 必须关 ENET(PG11/13/14),
            JP52 拨 (1,2)=TLI(SDIO 退化为 1-bit 模式),
            DAC0_OUT1(PA5) 让位给 LCD_R4。

    config BSP_USING_TOUCH
        bool "Enable GT911 touch (software I2C)"
        default n
        depends on BSP_USING_TLI

2.3.2、sconsript修改

~/gd32/rt-thread/bsp/gd32/arm/libraries/gd32_drivers/SConscript 将原SDIO段和SDRAM段改成如下:

py 复制代码
if GetDepend('BSP_USING_SDIO'):
    if GetDepend('SOC_SERIES_GD32H7xx'):
        src += ['drv_sdio_h7.c']
    else:
        src += ['drv_sdio.c']
if GetDepend('BSP_USING_SDRAM'):
    if GetDepend('SOC_SERIES_GD32H7xx'):
        src += ['drv_sdram_h7.c']
    else:
        src += ['drv_sdram.c']

添加如下(与SDIO以及SDRAM同级别)

py 复制代码
if GetDepend('BSP_USING_TLI'):
    if GetDepend('SOC_SERIES_GD32H7xx'):
        src += ['drv_tli_h7.c']

if GetDepend('BSP_USING_TOUCH'):
    if GetDepend('SOC_SERIES_GD32H7xx'):
        src += ['drv_touch_h7.c']

scons --menuconfig (Top) → Hardware Drivers Config → On-chip Peripheral Drivers -*- Enable SDRAM (EXMC DEVICE0, MT48LC16M16A2 32MB) \* Enable SDIO (TF card) \* Auto mount sd0 to / (event-driven watcher) \* Force SDIO 1-bit bus (JP52 shared with LCD) \* Enable sdprobe shell cmd (board without CD pin) \* Enable TLI LCD (480x272 RGB panel) \* Enable GT911 touch (software I2C)

三、应用程序

3.1 、一个sd卡文件系统自动挂载的补丁

~/gd32/rt-thread/bsp/gd32/arm/gd32h759i-eval/applications/sdcard_mnt.c

c 复制代码
/*
 * SD 卡挂载管理(DFS 挂载表 + 事件驱动自动挂载)
 *
 * 背景:mmcsd 的卡片识别在 mmcsd_detect 线程里异步完成,而 DFS 的
 * mount_table 在文件系统初始化阶段同步执行------此刻 sd0 块设备尚未
 * 注册,启动即挂载必然失败([E/DFS] mount fs[elm] on / failed)。
 * 这不是 bug,是时序错位。因此挂载表留空,挂载/卸载交给事件驱动
 * 的守护线程:框架在识别完成/移除时通过 mmcsd_wait_cd_changed()
 * 发事件,插入则挂载,移除则 umount(避免文件句柄悬空)。
 *
 * menuconfig 开关:
 *   BSP_SDIO_AUTO_MOUNT  自动挂载守护线程(本文件唯一功能主体)
 *
 * 放入 applications/ 即可编译(SConscript 默认收集该目录全部 .c)。
 */

#include <rtthread.h>
#include <dfs.h>
#include <dfs_fs.h>
/* RT_USING_DFS_MNTTABLE 在本工程被必选,本符号必须始终提供。
 * 故意留空:sd0 上电时还不存在;eMMC/SPI Flash 等上电即在的
 * 块设备可在此登记 */
const struct dfs_mount_tbl mount_table[] =
{
    {0}
};

#if defined(BSP_SDIO_AUTO_MOUNT)


#include <drivers/dev_mmcsd_core.h>     /* mmcsd_wait_cd_changed / MMCSD_HOST_PLUGED */

#define DBG_TAG             "app.sdmnt"
#define DBG_LVL             DBG_INFO
#include <rtdbg.h>

#define SD_DEVICE_NAME      "sd0"
#define SD_MOUNT_POINT      "/"
#define SD_FS_TYPE          "elm"

static void sdcard_mount(void)
{
    if (dfs_mount(SD_DEVICE_NAME, SD_MOUNT_POINT, SD_FS_TYPE, 0, 0) == 0)
        LOG_I("mount %s on %s ok", SD_DEVICE_NAME, SD_MOUNT_POINT);
    else
        LOG_E("mount %s on %s failed", SD_DEVICE_NAME, SD_MOUNT_POINT);
}

static void sdcard_mnt_thread(void *param)
{
    /* 线程启动与首次卡片识别存在竞争:若 sd0 已注册就直接挂,
     * 否则进入事件等待(识别完成的 PLUGED 事件随后必到)。
     * 直接挂载后必须把邮箱里那封"首次插入"事件排掉------它对应的
     * 就是刚才这次挂载,不排则进入循环后对同一挂载点重复挂载 */
    if (rt_device_find(SD_DEVICE_NAME) != RT_NULL)
    {
        sdcard_mount();
        mmcsd_wait_cd_changed(RT_WAITING_NO);
    }

    while (1)
    {
        rt_int32_t st = mmcsd_wait_cd_changed(RT_WAITING_FOREVER);

        if (st == MMCSD_HOST_PLUGED)
        {
            sdcard_mount();
        }
        else if (st == MMCSD_HOST_UNPLUGED)
        {
            if (dfs_unmount(SD_MOUNT_POINT) == 0)
                LOG_I("unmount %s ok", SD_MOUNT_POINT);
            else
                LOG_E("unmount %s failed", SD_MOUNT_POINT);
        }
    }
}

static int sdcard_mnt_init(void)
{
    rt_thread_t tid = rt_thread_create("sd_mnt", sdcard_mnt_thread, RT_NULL,
                                       2048, 21, 10);
    if (tid != RT_NULL)
        rt_thread_startup(tid);
    return 0;
}
INIT_APP_EXPORT(sdcard_mnt_init);

#endif /* BSP_SDIO_AUTO_MOUNT */

3.2、adc采集数据成 csv文件

c 复制代码
/*
 * CSV 数据记录仪:ADC2 采样 -> TF 卡 /adc_log.csv
 *
 * 串联两个战役的成果:
 *   - ADC 篇:rt_adc_enable / rt_adc_read(framework 路径,PC2_C 电位器)
 *   - SDIO 篇:sd0 自动挂载(app.sdmnt)+ DFS/POSIX 文件接口
 *
 * 用法(先确认 [I/app.sdmnt] mount sd0 on / ok):
 *   adc_csv 1000 10       # 采 1000 点,间隔 10ms
 *   adc_csv 500           # 采 500 点,默认间隔 1ms
 *
 * CSV 格式:index,tick,raw,mv ------ 电脑端 Excel/Python 直接打开画图。
 * 写卡节奏:每 32 行 fflush 一次(权衡掉电安全性与写放大)。
 */

#include <rtthread.h>
#include <rtdevice.h>
#include <stdio.h>

#define DBG_TAG             "app.csvlog"
#define DBG_LVL             DBG_INFO
#include <rtdbg.h>

#define CSVLOG_ADC_DEV      "adc2"
#define CSVLOG_ADC_CH       0
#define CSVLOG_FILE         "/adc_log.csv"
#define CSVLOG_FLUSH_LINES  32
#define CSVLOG_VREF_MV      3300
#define CSVLOG_ADC_MAX      4095        /* ADC2 为 12-bit */

static void adc_csv(int argc, char **argv)
{
    rt_adc_device_t adc;
    FILE *fp;
    rt_uint32_t count, interval = 1, i;
    rt_uint32_t raw, raw_min = CSVLOG_ADC_MAX, raw_max = 0;
    rt_uint64_t raw_sum = 0;
    rt_tick_t t0;

    if (argc < 2)
    {
        rt_kprintf("usage: adc_csv <count> [interval_ms]\n");
        return;
    }
    count = atoi(argv[1]);
    if (count == 0)
    {
        rt_kprintf("count must be > 0\n");
        return;
    }
    if (argc >= 3)
        interval = atoi(argv[2]);

    /* 前置检查:SD 卡挂载与 ADC 设备(错误前置,别采到一半才发现) */
    if (rt_device_find("sd0") == RT_NULL)
    {
        rt_kprintf("sd0 not found - TF card missing? (sdprobe to re-detect)\n");
        return;
    }
    adc = (rt_adc_device_t)rt_device_find(CSVLOG_ADC_DEV);
    if (adc == RT_NULL)
    {
        rt_kprintf("%s not found - enable BSP_USING_ADC2\n", CSVLOG_ADC_DEV);
        return;
    }

    fp = fopen(CSVLOG_FILE, "w");
    if (fp == RT_NULL)
    {
        rt_kprintf("fopen %s failed - card mounted? filesystem FAT32?\n",
                   CSVLOG_FILE);
        return;
    }

    rt_adc_enable(adc, CSVLOG_ADC_CH);
    fprintf(fp, "index,tick,raw,mv\n");

    rt_kprintf("logging %u samples @ %u ms -> %s ...\n", count, interval,
               CSVLOG_FILE);
    t0 = rt_tick_get();

    for (i = 0; i < count; i++)
    {
        raw = rt_adc_read(adc, CSVLOG_ADC_CH);
        fprintf(fp, "%u,%u,%u,%u\n", i, rt_tick_get() - t0, raw,
                raw * CSVLOG_VREF_MV / CSVLOG_ADC_MAX);

        if (raw < raw_min) raw_min = raw;
        if (raw > raw_max) raw_max = raw;
        raw_sum += raw;

        if ((i + 1) % CSVLOG_FLUSH_LINES == 0)
            fflush(fp);
        if (interval)
            rt_thread_mdelay(interval);
    }

    fclose(fp);     /* fclose 自带 flush,最后的残余行不会丢 */

    rt_kprintf("done: %u samples in %u ms, raw min/avg/max = %u/%u/%u\n",
               count, rt_tick_get() - t0, raw_min,
               (rt_uint32_t)(raw_sum / count), raw_max);
}
MSH_CMD_EXPORT_ALIAS(adc_csv, adc_csv,
                     log ADC2 samples to /adc_log.csv: adc_csv <count> [interval_ms]);

使用方法(记录1000个采样,采样间隔10ms到sd卡的根目录的 adc_log.csv ):

shell 复制代码
msh /> adc_csv 1000 10

3.3、 波形显示到屏幕

TIMER3 TRGO0 → TRIGSEL → ADC2 → DMA1 CH1 循环缓冲,软件触发(从 DMA 写指针前找中点上升沿,周期信号显示稳定不滚动),逐列重绘 ~20fps,触摸 RUN/STOP

c 复制代码
/*
 * 波形大屏显示(三战役会师应用):ADC2 DMA 采样 -> TLI LCD 实时波形
 *
 * 数据链路:TIMER3 TRGO0 -> TRIGSEL -> ADC2 -> DMA1 CH1 循环缓冲
 *           -> 软件触发找上升沿 -> 480 点/帧 -> LCD 逐列重绘
 *
 * 交互:
 *   触摸屏幕任意位置 = RUN / STOP 切换(示波器第一按钮)
 *   wave_show [fs]   启动(默认 51200Hz),再敲一次或 Ctrl+C 退出命令
 *
 * 接线:JP71 短接 + 杜邦线 PA5(已让位 LCD_R4? 注意!) <-> PC2_C
 *   ------ LCD 战役后 PA5 是 LCD_R4,闭环演示改用 VR1 电位器当信号源,
 *      或者把 dac_wave 的杜邦线拔掉、单看 ADC 悬浮噪声都可以。
 *
 * 设计要点:
 *   - 采样核心与 adc_dac_test.c 同款(TIMER/DMA 参数全部验证过),
 *     但用自己的 adc_buf,adc_sample 等命令互不干扰;
 *   - 软件触发:从 DMA 写指针向前找中点上升沿,周期信号显示稳定不滚动;
 *   - 逐列重绘(擦旧列->补网格->画新点),不整屏填充,600MHz 下 ~20fps;
 *   - 帧缓冲在 SDRAM(MPU non-cacheable),画完即所见,无需 cache 维护。
 */

#include <rtthread.h>
#include <rtdevice.h>
#include "gd32h7xx.h"
#include <stdlib.h>

#if defined(BSP_USING_TLI)

#define DBG_TAG             "app.wave"
#define DBG_LVL             DBG_INFO
#include <rtdbg.h>

extern void tli_draw_point(rt_uint16_t x, rt_uint16_t y, rt_uint16_t color);
extern void tli_draw_vline(rt_uint16_t x, rt_uint16_t y, rt_uint16_t len,
                           rt_uint16_t color);
extern void tli_fill(rt_uint16_t color);
extern rt_err_t gt911_read_point(rt_uint16_t *x, rt_uint16_t *y);

/* ===================== 配置区 ===================== */

#define WAVE_ADC_BUF_SIZE   1024        /* DMA 循环缓冲(2 的幂) */
#define WAVE_DEFAULT_FS     51200       /* 默认采样率 */
#define WAVE_FPS_MS         50          /* 帧间隔 ~20fps */

#define LCD_W               480
#define LCD_H               272
#define COLOR_BG            0x0000      /* 黑底 */
#define COLOR_GRID          0x4208      /* 深灰网格 */
#define COLOR_WAVE          0x07E0      /* 绿色波形(示波器经典配色) */
#define COLOR_MID           0xF800      /* 红色中位线 */

#define ADC_MID_CODE        2048        /* 12-bit 中点,软件触发电平 */

/* ===================== ADC2 DMA 采样管线(与 adc_dac_test.c 同款) ===================== */

static rt_uint16_t wave_adc_buf[WAVE_ADC_BUF_SIZE] __attribute__((aligned(32)));
static rt_uint16_t frame_buf[LCD_W];    /* 当前帧的 y 坐标(已换算) */

static void timer3_trgo_setup(rt_uint32_t update_freq)
{
    rt_uint32_t tclk, div, psc, arr;

    tclk = rcu_clock_freq_get(CK_APB1);
    {
        rt_uint32_t psc_field = GET_BITS(RCU_CFG0, 10, 12);   /* H7: BITS(10,12) */
        rt_bool_t mul4 = ((RCU_CFG1 & RCU_CFG1_TIMERSEL) != 0U);
        if (psc_field >= 4U)
            tclk *= (mul4 ? 4U : 2U);
        else if (mul4)
            tclk *= 2U;
    }

    div = tclk / update_freq;
    if (div == 0U) div = 1U;
    psc = 1U;
    while ((div / psc) > 65536U)
        psc <<= 1;
    arr = div / psc;
    if (arr == 0U) arr = 1U;

    rcu_periph_clock_enable(RCU_TIMER3);
    timer_deinit(TIMER3);
    timer_prescaler_config(TIMER3, psc - 1U, TIMER_PSC_RELOAD_NOW);
    timer_autoreload_value_config(TIMER3, arr - 1U);
    timer_master_output0_trigger_source_select(TIMER3, TIMER_TRI_OUT0_SRC_UPDATE);
    timer_enable(TIMER3);
}

static void wave_adc_start(rt_uint32_t fs)
{
    static rt_bool_t inited = RT_FALSE;   /* 一次性守卫:热重 init 会产生垃圾帧 */
    dma_single_data_parameter_struct cfg;

    if (inited) return;
    inited = RT_TRUE;

    rcu_periph_clock_enable(RCU_GPIOC);
    rcu_periph_clock_enable(RCU_ADC2);
    rcu_periph_clock_enable(RCU_DMA1);
    rcu_periph_clock_enable(RCU_DMAMUX);
    rcu_periph_clock_enable(RCU_TRIGSEL);

    adc_deinit(ADC2);
    adc_clock_config(ADC2, ADC_CLK_SYNC_HCLK_DIV6);
    adc_special_function_config(ADC2, ADC_SCAN_MODE, DISABLE);
    adc_special_function_config(ADC2, ADC_CONTINUOUS_MODE, DISABLE);
    adc_resolution_config(ADC2, ADC_RESOLUTION_12B);
    adc_data_alignment_config(ADC2, ADC_DATAALIGN_RIGHT);
    adc_channel_length_config(ADC2, ADC_REGULAR_CHANNEL, 1);
    adc_regular_channel_config(ADC2, 0, ADC_CHANNEL_0, 480);
    adc_external_trigger_config(ADC2, ADC_REGULAR_CHANNEL, EXTERNAL_TRIGGER_RISING);

    /* DMA1 CH1: ADC_RDATA -> wave_adc_buf, 循环(DMAMUX 通道 9,请求 123) */
    dma_deinit(DMA1, DMA_CH1);
    dma_single_data_para_struct_init(&cfg);
    cfg.request             = DMA_REQUEST_ADC2;
    cfg.periph_addr         = (rt_uint32_t)&ADC_RDATA(ADC2);
    cfg.periph_inc          = DMA_PERIPH_INCREASE_DISABLE;
    cfg.memory0_addr        = (rt_uint32_t)wave_adc_buf;
    cfg.memory_inc          = DMA_MEMORY_INCREASE_ENABLE;
    cfg.circular_mode       = DMA_CIRCULAR_MODE_ENABLE;
    cfg.direction           = DMA_PERIPH_TO_MEMORY;
    cfg.number              = WAVE_ADC_BUF_SIZE;
    cfg.priority            = DMA_PRIORITY_HIGH;
    dma_single_data_mode_init(DMA1, DMA_CH1, &cfg);
    /* 位宽字段无效,必须显式配置(否则 8-bit 字节复制) */
    dma_memory_width_config(DMA1, DMA_CH1, DMA_MEMORY_WIDTH_16BIT);
    dma_periph_width_config(DMA1, DMA_CH1, DMA_PERIPH_WIDTH_16BIT);
    DMAMUX_RM_CH9CFG = (DMAMUX_RM_CH9CFG & ~0x7FU) | 123U;
    dma_channel_enable(DMA1, DMA_CH1);

    trigsel_init(TRIGSEL_OUTPUT_ADC2_REGTRG, TRIGSEL_INPUT_TIMER3_TRGO0);

    adc_dma_mode_enable(ADC2);
    adc_dma_request_after_last_enable(ADC2);

    adc_enable(ADC2);
    rt_thread_mdelay(1);
    adc_calibration_mode_config(ADC2, ADC_CALIBRATION_OFFSET);
    adc_calibration_number(ADC2, ADC_CALIBRATION_NUM1);
    adc_calibration_enable(ADC2);

    timer3_trgo_setup(fs);
    LOG_I("wave adc pipeline started, fs = %lu Hz", fs);
}

/* ===================== 显示 ===================== */

/* 画一列的背景(黑底 + 该列可能压着的网格点/中线点) */
static void wave_erase_column(rt_uint16_t x)
{
    rt_uint16_t y;

    for (y = 0; y < LCD_H; y++)
    {
        rt_uint16_t c = COLOR_BG;
        if (y == LCD_H / 2)
            c = COLOR_MID;                          /* 中位线 1.65V */
        else if ((x % 48 == 0) || (y % 34 == 0))
            c = COLOR_GRID;
        tli_draw_point(x, y, c);
    }
}

static void wave_draw_frame(void)
{
    rt_uint16_t x;

    for (x = 0; x < LCD_W; x++)
    {
        wave_erase_column(x);
        tli_draw_point(x, frame_buf[x], COLOR_WAVE);
        /* 与前一列连线,波形不断裂 */
        if (x > 0)
        {
            rt_int32_t y0 = frame_buf[x - 1], y1 = frame_buf[x];
            if (y1 > y0 + 1)
                tli_draw_vline(x, y0 + 1, y1 - y0 - 1, COLOR_WAVE);
            else if (y0 > y1 + 1)
                tli_draw_vline(x, y1 + 1, y0 - y1 - 1, COLOR_WAVE);
        }
    }
}

/* 从循环缓冲取一帧:软件触发------从写指针前找中点上升沿,取其后 480 点 */
static void wave_acquire_frame(void)
{
    rt_uint32_t wpos, start, i;

    /* DMA 循环在跑,读前 invalidate(不停通道,停了后续全是陈旧数据) */
    rt_hw_cpu_dcache_ops(RT_HW_CACHE_INVALIDATE, wave_adc_buf,
                         sizeof(wave_adc_buf));
    wpos = WAVE_ADC_BUF_SIZE - (DMA_CHCNT(DMA1, DMA_CH1) & 0xFFFFU);

    /* 从写指针向前(旧数据方向)搜中点上升沿,最多搜半缓冲 */
    start = wpos;
    for (i = 1; i < WAVE_ADC_BUF_SIZE / 2; i++)
    {
        rt_uint32_t cur = (wpos - i) & (WAVE_ADC_BUF_SIZE - 1);
        rt_uint32_t pre = (wpos - i - 1) & (WAVE_ADC_BUF_SIZE - 1);

        if (wave_adc_buf[pre] < ADC_MID_CODE &&
            wave_adc_buf[cur] >= ADC_MID_CODE)
        {
            start = cur;
            break;
        }
    }

    for (i = 0; i < LCD_W; i++)
    {
        rt_uint32_t idx = (start + i) & (WAVE_ADC_BUF_SIZE - 1);
        rt_uint32_t code = wave_adc_buf[idx];

        if (code > 4095U) code = 4095U;
        /* code 0 -> 屏幕底部(LCD_H-1),code 4095 -> 顶部(0) */
        frame_buf[i] = (rt_uint16_t)((LCD_H - 1) -
                                     code * (LCD_H - 1) / 4095U);
    }
}

/* ===================== 命令 ===================== */

static void wave_show(int argc, char **argv)
{
    rt_uint32_t fs = WAVE_DEFAULT_FS;
    rt_bool_t running = RT_TRUE;
    rt_uint16_t tx = 0, ty = 0;
    rt_bool_t was_touched = RT_FALSE;

    if (argc >= 2)
        fs = (rt_uint32_t)atoi(argv[1]);

    wave_adc_start(fs);
    tli_fill(COLOR_BG);
    rt_kprintf("wave_show: fs=%lu Hz, touch screen = RUN/STOP\n", fs);

    while (1)
    {
        if (running)
        {
            wave_acquire_frame();
            wave_draw_frame();
        }

        /* 触摸 = RUN/STOP,带按下沿检测(按住不连发) */
#if defined(BSP_USING_TOUCH)
        if (gt911_read_point(&tx, &ty) == RT_EOK)
        {
            if (!was_touched)
            {
                running = !running;
                was_touched = RT_TRUE;
                rt_kprintf("%s\n", running ? "RUN" : "STOP");
            }
        }
        else
        {
            was_touched = RT_FALSE;
        }
#endif
        rt_thread_mdelay(WAVE_FPS_MS);
    }
}
MSH_CMD_EXPORT_ALIAS(wave_show, wave_show,
                     adc waveform on lcd: wave_show [fs]);

#endif /* BSP_USING_TLI */

使用方式(如果找不到模拟信号源,可以扭动RV1也可以看到屏幕上的波形线上下升降):

shell 复制代码
msh /> wave_show 200

正常开机到 wave_show后的日志如下:

shell 复制代码
 \ | /
- RT -     Thread Operating System
 / | \     5.2.2 build Sep 14 2026 23:03:42
 2006 - 2024 Copyright by RT-Thread team
lwIP-2.0.3 initialized!
[E/drv.enet] enet_init failed --- is the cable connected?
[E/kernel.device] To initialize device:e0 failed. The error code is -255
[I/drv.sdio] sdio0 host registered
[I/drv.tli] tli lcd 480x272 @ fb=0xc0000000
[I/drv.sdio] sdio clock 400000 Hz (div=500)
[I/SDIO] SD card capacity 31178752 KB.
[I/drv.sdio] sdio clock 25000000 Hz (div=8)
[I/SDIO] sd: switch to High Speed / SDR25 mode

[I/drv.sdio] sdio clock 50000000 Hz (div=4)
found part[0], begin: 1048576, size: 29.750GB
[W/drv.touch] gt911 id read failed (0xff 0xff 0xff), continue anyway
[I/drv.touch] gt911 ready (id=▒▒▒, ver=0xff)
can't find device:sd0 to be mounted.
build marker: v9 - keys + leds
ready: WAKEUP->LED1, TAMPER->LED2, USER->both
msh />[I/app.sdmnt] mount sd0 on / ok

msh />
msh />
msh />wave_show 200
[I/app.wave] wave adc pipeline started, fs = 200 Hz
wave_show: fs=200 Hz, touch screen = RUN/STOP

目标板屏幕显示内容如图(无信号源):

相关推荐
DolphinScheduler社区2 小时前
Apache DolphinScheduler 3.4.3 发布!权限安全与稳定性全面增强,调度补火即将上线
开源·agent·海豚调度·大数据工作流调度
FIT2CLOUD飞致云3 小时前
GPU监控支持华为昇腾设备,防火墙管理焕新,虚拟机管理下放至专业版,1Panel v2.3.0版本发布
运维·ai·开源·1panel·运维面板
dong_junshuai18 小时前
每天一个开源项目#100 OpenCodeReview:2.6万星的低噪声AI审查器
程序员·开源·github
caoerzhong18 小时前
JeeWMS 开源仓库管理系统权限与域验证解析:Java WMS 如何把数据权限管到仓、货主与人
java·python·开源·vue
caoerzhong18 小时前
JeeWMS 开源仓库管理系统多租户架构解析:一套 Java WMS 如何同时服务多个货主与多个仓库
java·架构·开源·vue
千桐科技21 小时前
qKnow 开源版 v2.4.3 更新解析:从知识数据接入到自定义解析与结果复用
开源·llm·agent·ai智能体·qknow·智能体构建
swithun1 天前
不用 WebView,我用 Kotlin + Compose Multiplatform 重写 Mermaid,并做了 2048 组对拍
android·开源·kotlin
Zwawa1 天前
重新认识结构体:为什么 STM32 库函数喜欢接收一大包参数
嵌入式