时间:2026-05-31 来源:FPGA_UCY 关于我们 0
用 SAI 做通用串行数据通信(而非音频),是 SAI 接口的典型“非音频”应用场景!RK3576 的 SAI 本质是“可编程串行同步通信控制器”,只要 FPGA 端按 SAI 的时序规则收发数据,就能实现稳定的双向数据传输。本教程会从“原理适配→硬件连接→设备树配置→代码开发→调试验证”一步步教你,全程避开音频相关冗余内容,聚焦“SAI 作为通用串口”的核心用法。
一、先搞懂核心:SAI 与 FPGA 通信的底层逻辑1. 为什么 SAI 能和 FPGA 通信?
SAI 的本质是同步串行通信接口(时钟+数据+帧同步),和 SPI/I2C 类似,只是时序规则不同。对 FPGA 来说,只要解析/生成 SAI 的时序,就能和 RK3576 交换数据,核心优势:
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 一致)
最简时序规则(新手必记):
LRCK 作为“帧起始”信号:LRCK 电平翻转时,开始 1 帧数据传输;BCLK 作为“位同步”信号:每 1bit 数据在 BCLK 的上升沿由发送端输出,下降沿由接收端采样;数据格式:每帧传输 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
必须共地!否则时钟/数据会有干扰,导致数据错误
关键提醒:
无需接 MCLK(MCLK 是给音频 CODEC 的时钟,通用通信不需要);若传输距离超过 10cm,建议在信号线上串 22Ω 电阻(减少反射);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 = ; // 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
#include
#include
#include
#include
#include
// 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. 硬件层面验证(优先做)2. 软件层面验证3. FPGA端验证六、常见问题排查(新手必看)
问题现象
大概率原因
解决方法
SAI无时钟输出(BCLK/LRCK)
设备树中SAI未启用,或寄存器配置错误
1. 确认设备树 status = "okay";2. 检查代码中 SAI_CTRL0 的“启用位”是否置1
FPGA接收数据乱码
位序/时钟沿配置错误
1. 确认SAI是“MSB先传”;2. FPGA在BCLK下降沿采样(和RK3576的发送沿相反)
数据丢包
DMA未启用,或发送速率过快
1. 设备树中必须配置DMA通道;2. 降低应用层发送速率(增大usleep值)
寄存器映射失败