0755-82922363

AD16N_1.4.1__TF卡和Flash共用引脚会导致程序在PC_mode下死机

AD16N_1.4.1__TF卡和Flash共用引脚会导致程序在PC_mode下死机

TF卡和Flash共用SPI:怎么接电脑当U

项目里用了杰理AD16N(一颗跑音频+蓝牙的小芯片,SOP16封装只有16个脚),T卡和外挂Flash得共用同一组SPI引脚。插上USB线进PC模式,电脑要同时看到T卡和Flash两个盘。三根线、两个设备、一条USB——其实跟独木桥一个道理:桥就一条,两辆车得轮着过,谁过桥谁就得把对面拦住。下面就按这个思路捋一遍。

 

整体思路

Plain Text
// TF卡+Flash共用SPI → USB接电脑的完整链路

板级配置(PA09/PA10/PA11)
  
  ├── 注册设备:sd0(T卡) + ext_flsh(Flash)
  
  ├── 进PC模式 → usb_slave_app()
  │   ├── dev_open("ext_flsh") → SPI仲裁 → Flash初始化
  │   ├── 预热读(sector 0、1)
  │   └── usb_start() → USB枚举
  
  ├── 电脑发SCSI命令(READ_10 / WRITE_10)
  │   ├── MSD层拿到LUN号 → 找到对应设备
  │   ├── IOCTL_CMD_RESUME → SPI仲裁抢桥
  │   ├── Flash读写(带4KB cache)
  │   └── IOCTL_CMD_SUSPEND → SPI仲裁还桥
  
  └── 退出PC模式 → dev_close() → 释放SPI

翻译:芯片上电先把两个设备都注册好,插USBPC模式后电脑看到两个盘,每次读写Flash前先把T卡的SPI线挂起来(拦桥),读写完再还回去(放行),来来回回就这么切。

 

一、硬件怎么共的——三根线两个人用

AD16NSOP16封装脚不够用,T卡和Flash只能共用PA09PA10PA11这三根线。两个设备看这三根线的角色不一样:

引脚

T卡视角(SD模式)

Flash视角(SPI模式)

PA09

CLK(时钟)

CS(片选)

PA10

DAT0(数据)

CLK(时钟)

PA11

CMD(命令)

DO/DI(数据出入)

 

Plain Text
// 共线拓扑——独木桥结构

        PA09     PA10     PA11
          │        │        │
    ┌─────┴────────┴────────┴─────┐
    │       SPI / SD 总线         │
    └─────┬──────────────┬────────┘
          │              │
    ┌─────┴─────┐  ┌─────┴────────┐
    │  T卡(SD)  │  │  Flash(SPI)  │
    │  CLK/DAT  │  │  CS/CLK/DO   │
    │  /CMD     │  │  /DI         │
    └───────────┘  └──────────────┘

三根线就是那条独木桥——同一时刻只能有一个设备在上面跑数据,另一个得让路。

整体架构放一张图:

 

看一下SDK里怎么配的:

Plain Text
// app_config.h —— SPI引脚和复用开关

#define TFG_SPI_UNIDIR_MODE_EN    ENABLE  //外挂Flash单线模式
#define TFG_SPI_WORK_MODE         SPI_MODE_UNIDIR_1BIT

#define TFG_SPI_CS_PORT_SEL       IO_PORTA_09
#define TFG_SPI_CLK_PORT_SEL      IO_PORTA_10
#define TFG_SPI_DO_PORT_SEL       IO_PORTA_11
#define TFG_SPI_DI_PORT_SEL       IO_PORTA_11  //单线模式DO和DI共用PA11

#define SPI_SD_IO_REUSE           TFG_SD_EN   //SD启用时才开IO复用协调

再看T卡那边的引脚定义:

Plain Text
// sd_port.h —— T卡引脚(和Flash完全重叠)

#define SDMMC_CMD_IO       IO_PORTA_11  //CMD → PA11
#define SDMMC_CLK_IO       IO_PORTA_09  //CLK → PA09
#define SDMMC_DAT_IO       IO_PORTA_10  //DAT → PA10

两段配置对着看:PA09/PA10/PA11一个不差,FlashSPIT卡的SD用的就是同一组脚。SPI_SD_IO_REUSE这个宏就是"独木桥交通灯"的总开关——T卡启用了,复用协调才会生效。

 

二、软件怎么仲裁的——独木桥上的交通灯

硬件共线了,软件得管好"谁在用桥"SDK里搞了一套信号量+SPI切换的机制,每次Flash要用SPI之前,先把T卡的IO挂起来,用完再还回去。

抢桥:norflash_reuse_enter()

Plain Text
// norflash.c —— Flash抢占SPI总线

static int norflash_reuse_enter()
{
    if (norflash_reuse_keep) {          //已经占着桥了,直接过
        return 0;
    }
    if (sd_io_reuse_suspend() != 0) {   //叫T卡让路,让不了就失败
        return -1;
    }
    spi_busy = 1;                       //标记:桥上有人了
    spi_flash_io_resume();              //把SPI控制器重新配好给Flash用
    return 0;
}

还桥:norflash_reuse_exit()

Plain Text
// norflash.c —— Flash归还SPI总线

static void norflash_reuse_exit()
{
    if (norflash_reuse_keep) {          //有"霸桥"标记,不还
        return;
    }
    if (!spi_busy) {                    //本来就没占,不用还
        return;
    }
    spi_flash_io_suspend();             //SPI引脚释放(IO设高阻)
    spi_busy = 0;                       //标记清除
    sd_io_reuse_resume();               //把T卡的IO还回去
}

T卡怎么让路:sd_io_reuse_suspend()

这是最底层的仲裁——逐线挂起T卡的三个IOCMDCLKDAT),每根线对应一把信号量:

Plain Text
// norflash.c —— 逐线挂起T卡IO

int sd_io_reuse_suspend()
{
    u8 sd_io_suspend_status = 0;
    u16 retry_cnt = 0;
_retry:
    OS_ENTER_CRITICAL();                //关中断,进临界区
    if (0 != sd_io_suspend(0, 0)) {     //挂CMD线
        goto _exit;
    }
    sd_io_suspend_status |= BIT(0);
    if (0 != sd_io_suspend(0, 1)) {     //挂CLK线
        goto _exit;
    }
    sd_io_suspend_status |= BIT(1);
    if (0 != sd_io_suspend(0, 2)) {     //挂DAT线
        goto _exit;
    }
    OS_EXIT_CRITICAL();
    return 0;                           //三根线全挂起,成功

_exit:
    //中途失败,已挂的线得还回去
    if (sd_io_suspend_status & BIT(0)) sd_io_resume(0, 0);
    if (sd_io_suspend_status & BIT(1)) sd_io_resume(0, 1);
    OS_EXIT_CRITICAL();

    if (retry_cnt++ < 10000) {          //最多重试10000次,每次等10μs
        udelay(10);
        goto _retry;
    }
    return -1;                          //100ms还没拿到,彻底失败
}

独木桥上的交通灯工作流程:Flash要过桥 先按住T卡三根信号线的"红灯"CMD→CLK→DAT逐个锁) → Flash过桥干活 干完把红灯放开 → T卡可以走了。整个过程在临界区里做,中断都关了,不会被打断。

SPI底层切换做了什么

"挂起""恢复"不只是改个标志位,SPI控制器得真正切换:

Plain Text
// spi.c —— SPI挂起(释放IO给T卡)

void spi_suspend(hw_spi_dev spi)
{
    u8 *port = spix_p_data_cache[spi].port;
    spi_io_port_uninit(port[0]);      //CLK → 高阻
    spi_io_port_uninit(port[1]);      //DO  → 高阻
    spi_io_port_uninit(port[2]);      //DI  → 高阻
    spi_io_crossbar_uninit(spi);      //断开IO矩阵映射
    spi_disable(spi_regs[spi]);       //关SPI外设
}

 

Plain Text
// spi.c —— SPI恢复(IO重新配给Flash)

void spi_resume(hw_spi_dev spi)
{
    spi_set_bit_mode(spi, spi_info->mode);   //重配单线/双线模式
    spi_cs_dis(spi_regs[spi]);               //CS手动控制
    spi_clk_idle_sel(...);                   //时钟极性
    spi_set_baud(spi, spi_info->clk);       //波特率
    spi_enable(spi_regs[spi]);               //开SPI外设
}

挂起=IO全放成高阻+SPI外设+IO矩阵,恢复=重配寄存器+重连IO矩阵+SPI。不是软件层面的"暂停",是硬件层面的"拆线重接"

整个仲裁时序放一张图,看起来更直观:

image.png

拿一张表汇总一下整个仲裁流程的状态变化:

阶段

spi_busy

TIO状态

SPI控制器

Flash可访问

空闲

0

正常(T卡用)

关闭

Flash抢桥中

0→1

逐线挂起

恢复

❌→✅

Flash使用中

1

挂起

开启

Flash还桥中

1→0

逐线恢复

关闭

✅→❌

 

三、怎么接电脑的——USB MSDFlashU

独木桥的仲裁搞定了,接下来看怎么让电脑认出这两个盘。USB Mass Storage用的是Bulk-Only Transport协议——电脑发SCSI命令,芯片收到后读写对应的存储设备,再把结果传回去。

注册两个盘

SDK里把T卡和Flash分别注册成USB MSD的两个LUN(逻辑单元号):

Plain Text
// msd.c —— 注册T卡和Flash为两个U盘

_msd_var.max_lun = 2;
_msd_var.inquiry[0] = SCSIInquiryData;       //LUN0: T卡
_msd_var.inquiry[1] = EX_SCSIInquiryData;    //LUN1: Flash

msd_register_disk("sd0", NULL);              //T卡
msd_register_disk("ext_flsh", NULL);         //外挂Flash

 

Plain Text
// device_list.c —— 设备注册表

//U盘模式用norflash_dev_ops(512字节块单位,自带擦写管理)
{.name = "ext_flsh", .ops = &norflash_dev_ops, .priv_data = &norflash_data},
//T卡用sd_dev_ops
{.name = "sd0", .ops = &sd_dev_ops, .priv_data = &sd0_data},

电脑插上USB看到两个盘:一个是T卡(LUN0),一个是FlashLUN1)。电脑不知道它们共用SPI,它只管发SCSI命令,芯片内部自己协调。

PC模式的完整流程

Plain Text
// usb_slave_mode.c —— PC模式入口

void usb_slave_app(void)
{
    usb_device_mode(0, 0);                //配置USB为从机模式

    void *device = dev_open("ext_flsh", 0);  //打开Flash设备
    if (device != NULL) {
        //开启4KB读缓存 + 关闭缓存同步的临界区保护
        dev_ioctl(device, IOCTL_SET_READ_USE_CACHE, 1);
        dev_ioctl(device, IOCTL_SET_CACHE_SYNC_ISR_EN, 0);

        //预热:读sector 0和1,把Flash叫醒+填充cache
        memset(usb_slave_flash_warmup_buf, 0, sizeof(usb_slave_flash_warmup_buf));
        dev_bulk_read(device, usb_slave_flash_warmup_buf, 0, 1);
        dev_bulk_read(device, usb_slave_flash_warmup_buf, 1, 1);
    }

    usb_start();  //启动USB枚举,电脑开始识别
    //... 消息循环处理SCSI命令 ...
}

PC模式就四步:配USB从机 打开Flash(触发一次SPI仲裁) 预热读两个扇区 启动USB。预热是为了把Flash从休眠里叫醒,顺便填好cache

电脑读写Flash的数据通路

电脑发一个SCSI READ_10命令,芯片这边的处理路径(同步模式):

Plain Text
// msd.c —— SCSI READ_10处理(同步路径)

static void read_10(const struct usb_device_t *usb_device)
{
    u32 lba = read_32(msd_var->cbw.lba);        //起始扇区号
    u16 lba_num = (cbw.LengthH << 8) | cbw.LengthL;  //扇区数

    while (lba_num) {
        num = lba_num > MSD_BLOCK_SIZE ? MSD_BLOCK_SIZE : lba_num;

        dev_ioctl(dev_fd, IOCTL_CMD_RESUME, 0);  //抢桥
        err = dev_bulk_read(dev_fd, msd_buf, lba, num);  //读Flash
        dev_ioctl(dev_fd, IOCTL_CMD_SUSPEND, 0);  //还桥

        msd_mcu2usb(usb_device, msd_buf, num * 512);  //数据传给电脑
        lba += num;
        lba_num -= num;
    }
}

写操作(WRITE_10)也是同样的套路,多了一个flush

Plain Text
// msd.c —— SCSI WRITE_10处理(同步路径,简化)

while (lba_num) {
    msd_usb2mcu_64byte_fast(..., msd_buf, num * 512);  //从USB收数据

    dev_ioctl(dev_fd, IOCTL_CMD_RESUME, 0);   //抢桥
    dev_bulk_write(dev_fd, msd_buf, lba, num); //写Flash
    dev_ioctl(dev_fd, IOCTL_CMD_SUSPEND, 0);   //还桥
}
if (need_flush) {
    dev_ioctl(dev_fd, IOCTL_CMD_RESUME, 0);
    dev_ioctl(dev_fd, IOCTL_FLUSH, 0);        //把cache里的脏数据刷到Flash
    dev_ioctl(dev_fd, IOCTL_CMD_SUSPEND, 0);
}

每次读写Flash的套路都是"抢桥干活还桥"三板斧。写完还得flush一下,把cache里攒着的脏数据真正写进Flash。电脑那边发 SYNCHRONIZE_CACHE命令时也会触发flush

 

四、Flash cache怎么工作的——桥上的临时仓库

Flash写入有个特点:得先擦后写,而且擦除是按4KB扇区整块来的。如果每写512字节就擦一次4KB,速度慢得离谱,Flash寿命也撑不住。SDK的做法是在RAM里开一个4KB缓冲区当"临时仓库"——数据先往仓库里攒,攒满一个扇区或者被催了(flush)才真正擦写一次。

cache的三个状态变量

Plain Text
// norflash.c —— Flash cache核心变量

static u8 *flash_cache_buf;        //4KB缓冲区(RAM里)
static u32 flash_cache_addr;       //当前缓存的是哪个4KB扇区
static u8  flash_cache_is_dirty;   //脏标记:缓冲区内容和Flash上不一样了
static u8  flash_cache_timer;      //脏计时:攒了多久没写

写入流程:攒着写

Plain Text
// norflash.c —— _norflash_write() 写入逻辑(简化)

//1. 算出写入地址落在哪个4KB扇区
u32 align_addr = addr / 4096 * 4096;

//2. 如果和当前缓存的扇区不一样,先把旧脏数据刷掉
if (align_addr != flash_cache_addr) {
    if (flash_cache_is_dirty) {
        _norflash_eraser(FLASH_SECTOR_ERASER, flash_cache_addr);  //擦旧扇区
        _norflash_write_pages(flash_cache_addr, flash_cache_buf, 4096);  //写回
        flash_cache_is_dirty = 0;
    }
    _norflash_read(align_addr, flash_cache_buf, 4096, 0);  //读新扇区到缓存
    flash_cache_addr = align_addr;
}

//3. 在缓冲区里改数据
memcpy(flash_cache_buf + (addr - align_addr), w_buf, align_len);

//4. 判断是否凑满一整个扇区
if ((addr + align_len) % 4096) {
    flash_cache_is_dirty = 1;   //没满,标脏,先不写
    flash_cache_timer = 1;      //开始计时
} else {
    //满了,立刻擦写
    _norflash_eraser(FLASH_SECTOR_ERASER, align_addr);
    _norflash_write_pages(align_addr, flash_cache_buf, 4096);
    flash_cache_is_dirty = 0;
}

写入逻辑就是"先攒后刷":数据塞进RAM缓冲区,凑满4KB整扇区就立刻擦写,没凑满就标个脏等着。跟你往快递箱里塞包裹一样——塞满了叫快递员来收,没满就先攒着。

完整的写入流程放一张图:

image.png

定时同步:别攒太久

如果一直没凑满怎么办?SDK有个定时器 _norflash_cache_sync_timer(),隔一段时间检查一次——有脏数据就强制刷盘:

Plain Text
// norflash.c —— 定时同步脏缓存(简化)

void _norflash_cache_sync_timer(u32 sync_step)
{
    if (flash_cache_is_dirty) {
        //同步也得抢桥!
        if (norflash_reuse_enter() == 0) {
            _norflash_eraser(FLASH_SECTOR_ERASER, flash_cache_addr);
            _norflash_write_pages(flash_cache_addr, flash_cache_buf, 4096);
            flash_cache_is_dirty = 0;
            norflash_reuse_exit();   //用完还桥
        }
    }
}

定时同步也得走独木桥仲裁——抢桥擦写还桥。所以就算Flash自己在后台刷缓存,T卡的SPI也会被短暂挂起。

 

 

五、会出什么问题——桥上堵车的几种情况

独木桥方案能跑,但有几个容易出问题的地方。

1:仲裁超时死机

sd_io_reuse_suspend()重试10000×10μs = 100msT卡如果在做大块读写,100ms内还没释放IO信号量,Flash这边就返回-1。上层拿到-1后的处理方式各不相同——有的直接返回0(读写失败但不崩),有的地方没做错误处理就直接空指针炸了

查了半天发现根因有两层:

 T卡在做连续多扇区读写时,一个命令周期可能超过100ms

 Flash驱动的 norflash_bulk_read拿到-1后返回0MSD层以为读了0个扇区,后续指针计算就错了

修法:

1.  sd_io_reuse_suspend()里把重试上限从10000调到更大的值(或者改成带超时的信号量等待)

2.  norflash_bulk_read/norflash_bulk_write失败路径加明确的错误码返回,MSD层收到错误码后回 MEDIUM_ERROR给电脑,不继续读写

Plain Text
// norflash.c —— 仲裁失败时的日志(排查用)

//抢桥失败会打印:
log_error("nf reuse enter fail keep=%d busy=%d", norflash_reuse_keep, spi_busy);

//等Flash BUSY超时会打印:
log_error("norflash_wait_ok timeout cmd=%x off=%x len=%x wr=%d sr1=%x busy=%d keep=%d",
          norflash_dbg_last_cmd, norflash_dbg_last_offset, norflash_dbg_last_len,
          norflash_dbg_last_is_write, reg_1, spi_busy, norflash_reuse_keep);

看到这两条log就知道是独木桥堵车了——要么T卡太忙让不了路,要么Flash操作太慢桥上堵住了。

2:异步MSD时序冲突

SDKmsd.c有个 USB_MSD_BULK_DEV_USE_ASYNC宏控制异步写入。异步模式下USB收数据和Flash写数据是重叠的(双缓冲乒乓),但SPI仲裁不是线程安全的——如果USB中断里触发了T卡检测,同时Flash又在写,两边同时操作SPI就会数据错乱

Plain Text
// msd.c —— 异步开关

#define USB_MSD_BULK_DEV_USE_ASYNC  0  //关闭异步MSD,走同步读写路径

这个宏设成0就是走同步路径:收完USB数据再写Flash,写完再收下一包。慢一点但安全。共用SPI的场景下别开异步,时序冲突太难排查。

3:写入慢(flush太频繁)

电脑往Flash盘拷文件时,每写几个扇区Windows就发一次 SYNCHRONIZE_CACHE命令。每次flush都要走"抢桥擦写4KB→还桥",而Flash擦除一个4KB扇区要几十ms。如果flush太频繁,写入速度就被擦除时间拖慢。

调了一下发现:

 不设缓存,每512字节都擦写一次:写入极慢,Flash寿命也撑不住

 缓存开了但 CACHE_SYNC_ISR_EN没关,定时器在中断里也会抢桥flush:写入时被打断,速度波动大

 IOCTL_SET_CACHE_SYNC_ISR_EN设成0(关闭中断内同步),只在SCSISYNCHRONIZE_CACHE和手动flush时才刷缓存:写入稳定,速度刚好

Plain Text
// usb_slave_mode.c —— PC模式下的缓存策略

dev_ioctl(device, IOCTL_SET_READ_USE_CACHE, 1);       //读缓存开
dev_ioctl(device, IOCTL_SET_CACHE_SYNC_ISR_EN, 0);    //关中断内自动同步

PC模式下得关掉中断内自动同步,不然后台定时器会抢桥flush,和前台的SCSI读写撞在一起。让flush只在SCSI命令触发时走,独木桥的通行顺序就可控了。

 

六、串联回顾——完整走一遍

从板子上电到电脑看到两个盘并读写Flash,整条链路用树枝图串起来:

Plain Text
// 完整数据链路——从配置到读写

板级配置
├── PA09/PA10/PA11 三脚共用(独木桥)
├── SPI_SD_IO_REUSE = 1(交通灯开关)

设备注册
├── "sd0" → sd_dev_ops(T卡驱动)
├── "ext_flsh" → norflash_dev_ops(Flash驱动,512B块模式+cache)

PC模式 usb_slave_app()
├── ① USB配从机模式
├── ② dev_open("ext_flsh") → norflash_reuse_enter()(第一次抢桥)
│   └── sd_io_reuse_suspend() → spi_flash_io_resume()
├── ③ 预热读sector 0、1(Flash叫醒+cache填充)
├── ④ usb_start()(USB枚举,电脑识别到两个盘)

电脑发SCSI命令
├── READ_10 / WRITE_10
│  ├── IOCTL_CMD_RESUME → norflash_reuse_enter()(抢桥)
│  ├── dev_bulk_read/write → SPI读写Flash(桥上干活)
│  │  └── 写入走4KB cache:攒满擦写 / 标脏等flush
│  └── IOCTL_CMD_SUSPEND → norflash_reuse_exit()(还桥)
├── SYNCHRONIZE_CACHE → IOCTL_FLUSH → 强制刷脏缓存

退出PC模式
└── dev_close("ext_flsh") → norflash_reuse_exit()(最后一次还桥)

回扣一下独木桥的类比:

现实中

代码里

独木桥(一条路)

PA09/PA10/PA11(三根共用线)

交通灯(红绿灯)

sd_io_reuse_suspend() / resume()

车辆通行证

spi_busy + norflash_reuse_keep

桥上施工(读写Flash

spi_flash_io_resume() + SPI命令

临时仓库(桥头堆货)

flash_cache_buf4KB RAM缓冲)

催收员(定时来收货)

_norflash_cache_sync_timer()

收费站(电脑USB口)

USB MSD Bulk-Only Transport

整条链路就这些——配置共脚、注册设备、仲裁抢桥、cache攒着写、USB传数据。搞明白"独木桥怎么轮着走"这一件事,剩下的都是工程细节。

 

感受

SPI共线本身不复杂,跟I2C多从机一个思路,分时复用嘛。SDK把仲裁封装成 norflash_reuse_enter/exit也算清晰,抢桥还桥看代码就能明白。USB MSD协议更不用说了,SCSI命令集翻一翻就上手。

卡住我比较久的是仲裁时序——T卡和FlashSPI切换牵扯IO矩阵、信号量、临界区,出了问题条件都凑不齐,很难复现。cache flush策略也绕了一阵,什么时候刷、在哪个上下文刷、会不会和前台SCSI命令撞车,翻了好几遍代码才理顺。最意外的是MSD异步双缓冲,宏定义看着挺美,放到共用SPI场景下直接没法用,这块的问题还是得等待杰理的AD小组来完善

共脚、仲裁、cacheMSD——搞明白这四个词,这套方案就算摸透了。

 


联系方式

地址:深圳市龙华区观湖街道观乐路5号多彩科创园B栋801

邮箱:steven@yunthinker.com