小白入门:RK3576 SAI 与 FPGA 通信开发教程

用 SAI 做通用串行数据通信(而非音频),是 SAI 接口的典型“非音频”应用场景!RK3576 的 SAI 本质是“可编程串行同步通信控制器”,只要 FPGA 端按 SAI 的时序规则收发数据,就能实现稳定的双向数据传输。本教程会从“原理适配→硬件连接→设备树配置→代码开发→调试验证”一步步教你,全程避开音频相关冗余内容,聚焦“SAI 作为通用串口”的核心用法。

一、先搞懂核心:SAI 与 FPGA 通信的底层逻辑

1. 为什么 SAI 能和 FPGA 通信?

SAI 的本质是同步串行通信接口(时钟+数据+帧同步),和 SPI/I2C 类似,只是时序规则不同。对 FPGA 来说,只要解析/生成 SAI 的时序,就能和 RK3576 交换数据,核心优势:

  • 高带宽:最高支持 384kHz 帧频率 × 32bit 位宽 = 12.288Mbps(比普通 UART 快得多);
  • 同步性好:基于时钟同步(BCLK),无丢包风险,适合FPGA这类对时序敏感的设备;
  • 多通道:TDM 模式下可同时传输多路数据(比如一路传指令、一路传数据)。

2. 核心时序(新手先掌握最简模式:I2S 主模式)

不用管音频协议,只需关注 3 个核心信号的时序规则(RK3576 做主机,FPGA 做从机,最易实现):

信号 方向(RK3576→FPGA) 作用
SAIx_BCLK 输出 位时钟(RK3576 生成,每 1 个 BCLK 对应 1bit 数据传输,频率可配置)
SAIx_LRCK 输出 帧同步时钟(RK3576 生成,每 1 个 LRCK 周期 = 1 帧数据,帧长度可配置)
SAIx_TXD 输出 发送数据(RK3576→FPGA,每bit数据在 BCLK 上升沿/下降沿稳定)
SAIx_RXD 输入 接收数据(FPGA→RK3576,规则和 TXD 一致)

最简时序规则(新手必记):

  1. LRCK 作为“帧起始”信号:LRCK 电平翻转时,开始 1 帧数据传输;
  2. BCLK 作为“位同步”信号:每 1bit 数据在 BCLK 的上升沿由发送端输出,下降沿由接收端采样;
  3. 数据格式:每帧传输 N bit 数据(比如 32bit),无需区分左右声道(把 LRCK 仅当作“帧同步”即可)。

3. 关键配置(避开音频相关,只保留通信核心)

配置项 推荐值(新手) 作用
工作模式 主模式(Master) RK3576 生成 BCLK/LRCK,FPGA 只需要被动响应,减少同步问题
协议类型 I2S 或 TDM I2S 时序最简单(优先选),TDM 适合多通道
帧长度(LRCK) 32bit/帧 每帧传输 32bit 数据(可自定义,比如 16/8bit)
BCLK 频率 8MHz(示例) 32bit/帧 × 250kHz 帧频率 = 8MHz BCLK(频率可通过寄存器配置)
数据位序 MSB 先传 高位在前,FPGA 解析更简单

二、硬件连接(最简配置,新手必对)

以 RK3576 的 SAI1 为例,和 FPGA 只需要 5 根线(无需音频 CODEC,直接对接):

RK3576 SAI1 引脚 FPGA 引脚 备注
SAI1_BCLK 任意IO(输入) FPGA 作为从机,接收 RK3576 生成的位时钟
SAI1_LRCK 任意IO(输入) FPGA 接收帧同步时钟
SAI1_TXD 任意IO(输入) FPGA 接收 RK3576 发送的数据
SAI1_RXD 任意IO(输出) FPGA 发送数据到 RK3576(若只需单向通信,可悬空)
GND GND 必须共地!否则时钟/数据会有干扰,导致数据错误

关键提醒:

  1. 无需接 MCLK(MCLK 是给音频 CODEC 的时钟,通用通信不需要);
  2. 若传输距离超过 10cm,建议在信号线上串 22Ω 电阻(减少反射);
  3. RK3576 的 SAI 引脚是 1.8V 电平,若 FPGA 是 3.3V,需加电平转换芯片(比如 TXS0108),避免烧引脚。

三、核心步骤 1:设备树配置(关键!避开音频,仅启用SAI硬件)

不用配置任何音频相关节点(比如 sound/CODEC),只需启用 SAI 控制器、配置引脚和基础参数,示例如下(以 SAI1 为例):

1. 启用 SAI1 控制器(禁用音频相关)

sai1: sai@fe470000 {
    compatible = "rockchip,rk3576-sai";  // 仅匹配SAI硬件驱动,不关联音频
    reg = <0x0 0xfe470000 0x0 0x1000>;   // SAI1寄存器基地址(查TRM确认)
    interrupts = <GIC_SPI 104 IRQ_TYPE_LEVEL_HIGH>;  // SAI1中断号(查TRM)
    // 时钟配置:仅启用基础时钟,不用音频PLL
    clocks = <&cru PCLK_SAI1>, <&cru SCLK_SAI1>, <&cru HCLK_SAI1>;
    clock-names = "pclk", "sclk", "hclk";
    // DMA配置:必须启用(SAI大量数据传输依赖DMA)
    dmas = <&dmac1 22>, <&dmac1 23>;     // TX/RX DMA通道(查TRM)
    dma-names = "tx", "rx";
    // 引脚复用:绑定SAI1的物理引脚(查开发板引脚手册)
    pinctrl-names = "default";
    pinctrl-0 = <&sai1_bclk>, <&sai1_lrck>, <&sai1_txd>, <&sai1_rxd>;
    status = "okay";  // 启用SAI1
    
    // 自定义属性:告诉驱动这是通用通信(非音频)
    rockchip,sai-mode = "general";       // 自定义字段,后续代码会读取
    rockchip,frame-length = <32>;        // 每帧32bit
    rockchip,bclk-freq = <8000000>;      // BCLK频率8MHz
};

// 引脚复用配置(关键:确保引脚设为SAI功能,而非GPIO)
pinctrl {
    sai1 {
        sai1_bclk: sai1-bclk {
            rockchip,pins = <1 RK_PA1 0 &pcfg_pull_none>;  // 示例引脚,改你自己的
        };
        sai1_lrck: sai1-lrck {
            rockchip,pins = <1 RK_PA2 0 &pcfg_pull_none>;
        };
        sai1_txd: sai1-txd {
            rockchip,pins = <1 RK_PA3 0 &pcfg_pull_none>;
        };
        sai1_rxd: sai1-rxd {
            rockchip,pins = <1 RK_PA4 0 &pcfg_pull_none>;
        };
    };
};

2. 编译烧录设备树

和之前音频场景一样,编译后烧录到开发板:

# 交叉编译设备树(替换为你的DTS文件名)
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- rk3576-evb.dtb -j8
# 烧录
fastboot flash dtb rk3576-evb.dtb

四、核心步骤 2:驱动层/应用层代码开发(新手先从应用层入手)

1. 核心思路(新手优先)

不用修改内核 SAI 驱动!RK3576 的 SAI 驱动已提供寄存器操作接口DMA 数据传输接口,直接在应用层通过 mmap 映射 SAI 寄存器,或通过 sysfs/dev 节点收发数据即可。

2. 最简应用层代码(SAI 发送数据到 FPGA)

功能:配置 SAI1 为主模式,按 32bit/帧、8MHz BCLK 发送自定义数据(比如 0x12345678),FPGA 端按时序接收即可。

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdint.h>

// SAI1寄存器基地址(从TRM查:RK3576 SAI1基地址是0xFE470000)
#define SAI1_BASE_ADDR 0xFE470000
#define SAI_REG_SIZE   0x1000

// SAI寄存器偏移(查TRM:Serial Audio Interface章节)
#define SAI_CTRL0      0x00  // 控制寄存器0(模式、时钟、位宽)
#define SAI_CTRL1      0x04  // 控制寄存器1(帧长度、同步模式)
#define SAI_TX_DATA    0x20  // 发送数据寄存器
#define SAI_STATUS     0x10  // 状态寄存器(判断发送完成)

int main() {
    int fd;
    uint8_t *sai_regs;
    uint32_t data = 0x12345678;  // 要发送给FPGA的数据(32bit)

    // 1. 打开/dev/mem(用于映射物理寄存器)
    fd = open("/dev/mem", O_RDWR | O_SYNC);
    if (fd < 0) {
        perror("open /dev/mem failed");
        return -1;
    }

    // 2. 映射SAI1寄存器到用户空间
    sai_regs = (uint8_t *)mmap(NULL, SAI_REG_SIZE, PROT_READ | PROT_WRITE,
                               MAP_SHARED, fd, SAI1_BASE_ADDR);
    if (sai_regs == MAP_FAILED) {
        perror("mmap failed");
        close(fd);
        return -1;
    }

    // 3. 配置SAI1为通用串行通信模式(核心!)
    // 3.1 复位SAI1(先清0)
    *(volatile uint32_t *)(sai_regs + SAI_CTRL0) = 0x00000000;
    usleep(1000);

    // 3.2 配置核心参数(按TRM寄存器说明)
    // CTRL0:主模式 + 32bit位宽 + BCLK上升沿发送 + MSB先传
    *(volatile uint32_t *)(sai_regs + SAI_CTRL0) = 
        (1 << 0) |   // 启用SAI
        (1 << 1) |   // 主模式(RK3576生成BCLK/LRCK)
        (0 << 3) |   // 数据位宽:32bit(TRM中查对应值)
        (1 << 8) |   // MSB先传
        (1 << 10);   // BCLK上升沿发送数据

    // CTRL1:帧长度32bit + LRCK帧同步
    *(volatile uint32_t *)(sai_regs + SAI_CTRL1) = 
        (31 << 0) |  // 帧长度:32bit(0~31共32位)
        (0 << 16);   // LRCK作为帧同步

    // 4. 循环发送数据到FPGA
    printf("SAI1 start send data to FPGA...\n");
    while (1) {
        // 等待发送寄存器为空
        while (*(volatile uint32_t *)(sai_regs + SAI_STATUS) & (1 << 0));
        
        // 写入要发送的数据(32bit)
        *(volatile uint32_t *)(sai_regs + SAI_TX_DATA) = data;
        
        // 数据自增(方便FPGA验证)
        data++;
        usleep(1000);  // 控制发送速率(可根据需求调整)
    }

    // 5. 释放资源(实际不会执行,仅示例)
    munmap(sai_regs, SAI_REG_SIZE);
    close(fd);
    return 0;
}

3. 代码编译与运行

# 交叉编译(针对RK3576的arm64架构)
aarch64-linux-gnu-gcc sai_fpga_tx.c -o sai_fpga_tx
# 传到开发板后运行(需要root权限)
chmod +x sai_fpga_tx
sudo ./sai_fpga_tx

4. FPGA端最简接收逻辑(Verilog示例)

FPGA 只需按 SAI 时序解析数据即可,核心代码如下(新手可直接用):

module sai_rx (
    input         SAI_BCLK,    // 来自RK3576的位时钟
    input         SAI_LRCK,    // 来自RK3576的帧同步
    input         SAI_TXD,     // 来自RK3576的数据
    output reg [31:0] rx_data, // 解析后的32bit数据
    output reg        rx_valid  // 数据有效标志
);

reg [5:0] bit_cnt;  // 位计数器(0~31)
reg       lrck_prev;

// 帧同步检测:LRCK翻转时重置计数器
always @(posedge SAI_BCLK) begin
    lrck_prev <= SAI_LRCK;
    if (SAI_LRCK != lrck_prev) begin
        bit_cnt <= 6'd0;
        rx_valid <= 1'b0;
    end else begin
        // 按位接收数据(MSB先传)
        rx_data[31 - bit_cnt] <= SAI_TXD;
        bit_cnt <= bit_cnt + 1'b1;
        
        // 32bit接收完成,置位有效标志
        if (bit_cnt == 6'd31) begin
            rx_valid <= 1'b1;
        end
    end
end

endmodule

五、调试验证(新手必做,避免踩坑)

1. 硬件层面验证(优先做)

  • 用示波器/逻辑分析仪抓信号:
    1. 先看 SAI_BCLK:是否为8MHz(频率稳定);
    2. 再看 SAI_LRCK:是否按32bit周期翻转(每32个BCLK翻转一次);
    3. 最后看 SAI_TXD:是否在BCLK上升沿变化,数据是否自增(和代码中一致)。
  • 若信号异常:
    • 检查设备树引脚配置(是否真的设为SAI功能,而非GPIO);
    • 检查硬件接线(共地是否接好,电平是否匹配)。

2. 软件层面验证

  • 检查SAI驱动加载:dmesg | grep sai → 看到“registered SAI1”即可;
  • 检查寄存器映射:运行代码后无“mmap failed”,说明寄存器映射成功;
  • 临时停止发送:ps -ef | grep sai_fpga_tx → kill 进程即可。

3. FPGA端验证

  • 查看 rx_valid 信号:每32个BCLK周期置位一次,说明帧同步正常;
  • 查看 rx_data:是否为连续自增的数值(0x12345678→0x12345679→…),说明数据接收正确。

六、常见问题排查(新手必看)

问题现象 大概率原因 解决方法
SAI无时钟输出(BCLK/LRCK) 设备树中SAI未启用,或寄存器配置错误 1. 确认设备树 status = "okay";2. 检查代码中 SAI_CTRL0 的“启用位”是否置1
FPGA接收数据乱码 位序/时钟沿配置错误 1. 确认SAI是“MSB先传”;2. FPGA在BCLK下降沿采样(和RK3576的发送沿相反)
数据丢包 DMA未启用,或发送速率过快 1. 设备树中必须配置DMA通道;2. 降低应用层发送速率(增大usleep值)
寄存器映射失败 权限不足,或地址错误 1. 用sudo运行代码;2. 核对SAI1基地址(TRM中确认是0xFE470000)

七、进阶优化(新手学会基础后再做)

  1. 双向通信:在上述代码基础上,添加 SAI_RX_DATA 寄存器读取逻辑,实现FPGA→RK3576数据传输;
  2. 多通道传输:启用TDM模式,把LRCK分成多个子通道(比如前16bit传指令,后16bit传数据);
  3. 中断方式收发:不用轮询寄存器,通过SAI中断(数据接收完成/发送空)触发数据处理,降低CPU占用;
  4. 更高带宽:提高BCLK频率(比如到16MHz),或增加帧长度(比如64bit),提升传输速率。

总结

  1. RK3576 SAI 与 FPGA 通信的核心是抛弃音频协议,把SAI当作“同步串行控制器”,只需关注时钟、帧同步、数据三个信号的时序;
  2. 新手优先用“SAI主模式+32bit帧长+MSB先传”,FPGA端按BCLK/LRCK时序解析即可,无需复杂配置;
  3. 问题排查优先查“硬件时序”(示波器抓信号),再查“寄存器配置”,驱动无需修改。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐