目录
一、SCCB协议
(1)SCCB时序
(2)与I2C的区别
二、Verilog 实现
(1)设计要求
(2)设计要点
(3)模块完整代码
三、功能验证
(1)写数据
(2)读数据
一、SCCB协议
SCCB(Serial Camera Control Bus) 是一种用于摄像头模块配置的通信协议(如 ov5640摄像头),由 OmniVision 公司提出。它类似于 I2C 协议,时钟线 SIO_C 即 SCL,数据线 SIO_D 即 SDA。但在某些细节上有所不同。SCCB 主要用于配置摄像头传感器的寄存器,例如设置分辨率、帧率、曝光时间等。
本文目的在于设计一个适用于 ov5640 摄像头模块的 SCCB 控制器,以实现对 ov5640 寄存器的读写操作,属于双线SCCB的连接方式。


(1)SCCB时序
由于SCCB与I2C时序非常相似,这里只简单介绍一下:
主设备向从设备特定地址写入数据(三相写),数据线上依次为:起始位、7bit从地址+写命令0、X、8bit字节地址、X、8bit写入数据、X、停止位(X指不关心,不同于I2C需要ACK应答)
主设备读取从设备特定地址数据(两相写+两相读),数据线上依次为:起始位、7bit从设备地址+写命令0、X、8bit字节地址、X、停止位2、起始位2、7bit从设备地址+读命令1、X、8bit读取数据、无应答、停止位
(蓝色表示读写操作都有的公共状态,红色表示各自的特殊状态,与后面状态机的设计有关系)
(2)与I2C的区别
1.写数据:不同于I2C需要ACK应答,用X代替,表示不关心该位数据。
2.读数据:同样用X代替ACK应答位。同时在第二次起始位前多了一个中间停止位。
二、Verilog 实现
(1)设计要求
1. 实现对 ov5640 寄存器的读写操作 (ov5640 设备7位地址:7'b0111_100)
2. 速度采用400KHz
(2)设计要点
设计要点其实和I2C控制器没有区别,主要为将应答位ACK修改为X、添加中间停止位状态、状态跳转顺序的调整。所以只需要在 I2C 控制器的基础上进行一些修改,因此这里不再赘述,而I2C控制器的设计可以参考我另一篇笔记 Verilog:I2C控制器 (代码也是根据这个进行修改)
需要特别注意的是 ov5640 的寄存器地址均为16位即两个字节,因此写寄存器地址时需要分两次写,先写高8位再写低8位,不要忘了中间还有一个X位(ACK)。
(3)模块完整代码
`timescale 1ns / 1ps
// 适用于ov5640 SCCB通信 寄存器(字节)地址16位,数据8位
module SCCB_ctrl(
input wire clk, //系统时钟100MHz
input wire rst_n, //复位
inout wire sda, //双向数据线(inout)
output wire scl, //输出时钟线
input wire rw_ctrl, //读写使能信号(0写1读)
input wire work_start, //SCCB启动信号
input wire [6:0] slave_addr, //7bit从设备地址
input wire [15:0] byte_addr, //16bit字地址
input wire [7:0] w_data, //8bit待写数据
output reg [7:0] r_data, //8bit读取数据
output reg work_done //SCCB读写完成信号
);
//sda传输方向控制
reg sda_oe; //sda输出使能,为1表示sda作输出
reg sda_out; //sda输出信号线
wire sda_in; //sda输入寄存器
assign sda_in = sda; //sda作输入直接读
assign sda = sda_oe ? (sda_out ? 1'bz : 1'b0) : 1'bz; //作输入需确保总线信号互不干扰对外呈高阻态,空闲和输出1时输出高阻态,因为sda线有上拉电阻
//状态机参数
reg [4:0] state; //当前状态
reg [4:0] next_state; //下一状态
localparam //--------------------------------------------公共状态
IDLE = 5'd0, //空闲
START = 5'd1, //起始位
W_SLAVE_ADDR = 5'd2, //写7位从设备地址+写命令0
ACK1 = 5'd3, //应答1
W_H_BYTE_ADDR = 5'd4, //写高8位字地址
ACK2 = 5'd5, //应答2
W_L_BYTE_ADDR = 5'd6, //写低8位字地址
ACK3 = 5'd7, //应答3(状态转移时进行读写判断)
STOP = 5'd8, //停止位
//--------------------------------------------写操作特殊状态
W_DATA = 5'd9, //写8位数据
W_ACK = 5'd10, //写应答
//--------------------------------------------读操作特殊状态
STOP2 = 5'd11, //中间停止位
START2 = 5'd12, //中间起始位
R_SLAVE_ADDR = 5'd13, //写7位从设备地址+读命令1
R_ACK = 5'd14, //读应答
R_DATA = 5'd15, //读8位数据位
N_ACK = 5'd16; //无应答
//计数器及参数
reg clk_div;
reg [7:0] cnt_clk; //分频计数
reg [3:0] cnt_bit; //位计数器
localparam cnt_max_400khz = 8'd125; //400khz分频翻转计算值
wire scl_half_1;
wire scl_half_0;
wire scl_ack_jump;
assign scl_half_1 = (cnt_clk == cnt_max_400khz >> 1 && clk_div==1'b1); //scl高电平中点(起始位、ACK接收、停止位时刻)
assign scl_half_0 = (cnt_clk == cnt_max_400khz >> 1 && clk_div==1'b0); //scl低电平中点(数据读写时刻)
assign scl_ack_jump=((cnt_clk ==(cnt_max_400khz >> 1)-5) && clk_div==1'b0); //scl低电平中点前5clk周期---
//---(ACK状态的下一状态跳转时刻,因为跳转都是由输入转输出状态,快一周期让输出状态赶上紧跟着的第一个scl_half_0,避免错过第1位数据)
//数据寄存器
reg [7:0] w_data_buf; //写入数据寄存器
reg [7:0] r_data_buf; //读出数据寄存器
reg [7:0] w_slave_addr_buf; //从设备地址寄存器(地址存高7位,0位为写命令0)
reg [7:0] r_slave_addr_buf; //从设备地址寄存器(地址存高7位,0位为读命令1)
reg [7:0] H_byte_addr_buf; //字地址高8位寄存器
reg [7:0] L_byte_addr_buf; //字地址低8位寄存器
reg work_en; //工作使能信号
//*************************************** MAIN CODE ***************************************//
//数据复位、开始工作时寄存数据(避免传输中途数据不稳定)
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
w_slave_addr_buf <= 8'b0000_0000;//0位为写命令0
r_slave_addr_buf <= 8'b0000_0001;//0位为读命令1
H_byte_addr_buf <= 8'b0;
L_byte_addr_buf <= 8'b0;
w_data_buf <= 8'b0;
end else if (work_start) begin
w_slave_addr_buf [7:1] <= slave_addr; //地址存高7位
r_slave_addr_buf [7:1] <= slave_addr; //地址存高7位
w_data_buf <= w_data;
H_byte_addr_buf <= byte_addr[15:8];
L_byte_addr_buf <= byte_addr[7:0];
end
end
//分频计数器(400khz时钟scl)
always @(posedge clk or negedge rst_n) begin
if (!work_en || !rst_n) begin
cnt_clk <= 8'd1;
clk_div <= 1'b1;
end else if (cnt_clk == cnt_max_400khz) begin
cnt_clk <= 8'd1;
clk_div <= ~clk_div;
end else
cnt_clk <= cnt_clk + 8'd1;
end
assign scl = clk_div;
//两段式状态机
always @(posedge clk or negedge rst_n) begin //state状态转移
if (!rst_n)
state <= IDLE;
else
state <= next_state;
end
always @(posedge clk or negedge rst_n) begin //state状态执行操作
if (!rst_n) begin
sda_oe <= 1'b0;//sda默认不使能输出(高阻态经上拉为1)
sda_out <= 1'b1;//sda默认输出1避免输出0
work_en <= 1'b0;
work_done <= 1'b0;
cnt_bit <= 4'd0;
next_state <= IDLE;
end else
case(state)
//---------------------空闲----------------------//
IDLE: begin
sda_oe <= 1'b0;
sda_out <= 1'b1;
work_done <= 1'b0;
if (work_start) begin //开始工作
work_en <= 1'b1; //工作使能信号work_en(工作时持续为1)
next_state <= START;
end
end
//--------------------起始位1--------------------//
START: begin
sda_oe <= 1'b1;//sda输出使能
if (scl_half_1) begin
sda_out <= 1'b0;//sda输出起始位0
next_state <= W_SLAVE_ADDR;
end
end
//--------------7bit从地址+写命令0---------------//
W_SLAVE_ADDR: begin
sda_oe <= 1'b1;//sda输出使能
if (scl_half_0) begin
if (cnt_bit != 4'd8) begin
sda_out <= w_slave_addr_buf[7-cnt_bit];//sda输出设备地址(从高到低)
cnt_bit <= cnt_bit + 4'd1;
end else begin
next_state <= ACK1;
cnt_bit <= 4'd0;
end
end
end
//--------------------应答1---------------------//
ACK1: begin
sda_oe <= 1'b0;//sda输出失能作输入
if (scl_ack_jump)
next_state <= W_H_BYTE_ADDR;
end
//-----------------高8bit字节地址-----------------//
W_H_BYTE_ADDR: begin
sda_oe <= 1'b1;//sda输出使能
if (scl_half_0) begin
if (cnt_bit != 4'd8) begin
sda_out <= H_byte_addr_buf[7-cnt_bit];//sda输出字节地址(从高到低)
cnt_bit <= cnt_bit + 4'd1;
end else begin
next_state <= ACK2;
cnt_bit <= 4'd0;
end
end
end
//--------------------应答2---------------------//
ACK2: begin
sda_oe <= 1'b0;//sda输出失能作输入
if (scl_ack_jump)
next_state <= W_L_BYTE_ADDR;
end
//-----------------低8bit字节地址-----------------//
W_L_BYTE_ADDR: begin
sda_oe <= 1'b1;//sda输出使能
if (scl_half_0) begin
if (cnt_bit != 4'd8) begin
sda_out <= L_byte_addr_buf[7-cnt_bit];//sda输出字节地址(从高到低)
cnt_bit <= cnt_bit + 4'd1;
end else begin
next_state <= ACK3;
cnt_bit <= 4'd0;
end
end
end
//--------------------应答3---------------------//
ACK3: begin
sda_oe <= 1'b0;//sda输出失能作输入
if (scl_ack_jump) begin
next_state <= rw_ctrl ? STOP2 : W_DATA; //读写操作判断(0写1读)
sda_out <= rw_ctrl ? 1'b0 : sda_out; //停止位要输出0跳1,转状态时提前置0
end
end
//--------------------停止位--------------------//
STOP: begin
sda_oe <= 1'b1;//sda输出使能
if (scl_half_1) begin
sda_out <= 1'b1;
work_done <= 1'b1;//工作结束信号置1(在STOP转IDLE时清0)
work_en <= 1'b0;//工作使能信号置0
next_state <= IDLE;
end
end
//----------------写操作特殊状态-----------------//
//-----------------8bit写入数据-----------------//
W_DATA: begin
sda_oe <= 1'b1;//sda输出使能
if (scl_half_0) begin
if (cnt_bit != 4'd8) begin
sda_out <= w_data_buf[7-cnt_bit];//sda输出写入数据(从高到低)
cnt_bit <= cnt_bit + 4'd1;
end else begin
next_state <= W_ACK;
cnt_bit <= 4'd0;
end
end
end
//-------------------写应答---------------------//
W_ACK: begin
sda_oe <= 1'b0;//sda输出失能作输入
if (scl_ack_jump) begin
sda_out <= 1'b0; //停止位要输出0跳1,转状态时提前置0
next_state <= STOP;
end
end
//----------------读操作特殊状态-----------------//
//------------------中间停止位------------------//
STOP2: begin
sda_oe <= 1'b1;//sda输出使能
if (scl_half_1) begin
sda_out <= 1'b1;
next_state <= START2;
end
end
//-------------------起始位2--------------------//
START2: begin
sda_oe <= 1'b1;//sda输出使能
if (scl_half_1) begin
sda_out <= 1'b0;//sda输出起始位0
next_state <= R_SLAVE_ADDR;
end
end
//--------------7bit从地址+读命令1---------------//
R_SLAVE_ADDR: begin
sda_oe <= 1'b1;//sda输出使能
if (scl_half_0) begin
if (cnt_bit != 4'd8) begin
sda_out <= r_slave_addr_buf[7-cnt_bit];//sda输出设备地址(从高到低)
cnt_bit <= cnt_bit + 4'd1;
end else begin
next_state <= R_ACK;
cnt_bit <= 4'd0;
end
end
end
//-------------------读应答---------------------//
R_ACK: begin
sda_oe <= 1'b0;//sda输出失能作输入
if (scl_ack_jump)
next_state <= R_DATA;
end
//-----------------8bit读取数据-----------------//
R_DATA: begin
sda_oe <= 1'b0;//sda输出失能作输入
if (scl_half_1 && cnt_bit!=4'd8) begin
r_data_buf[7-cnt_bit] <= sda_in;//sda在scl高电平中点读取数据(从高到低)
cnt_bit <= cnt_bit + 4'd1;
end
if (scl_ack_jump && cnt_bit==4'd8) begin //提前转状态,因为无应答会在scl_half_0输出
next_state <= N_ACK;
cnt_bit <= 4'd0;
r_data <= r_data_buf;//从寄存器取出读取的数据
end
end
//--------------------无应答--------------------//
N_ACK: begin
sda_oe <= 1'b1;//sda输出使能
if (scl_half_0)
sda_out <= 1'b1;
if (scl_ack_jump) begin
sda_out <= 1'b0;//停止位要输出0跳1,转状态时提前置0
next_state <= STOP;
end
end
default: next_state <= IDLE;
endcase
end
endmodule
三、功能验证
验证方法:
SCCB 读写 ov5640 的仿真略显麻烦,还需一个从设备模块以模拟 ov5640 行为。因此我选择直接上板调试:用拨码开关进行读写控制、写入数据设置,按钮发送工作启动信号 (按钮经过软件消抖处理),进行一次写数据操作后再将进行读数据操作,并将数据返回到LED灯上进行验证,同时调用Vivado的ILA进行在线调试,抓取波形检查时序。(关于ILA的使用可以参考我的另一篇笔记:Vivado:使用 ILA 进行在线调试 )
FPGA开发板上的操作及现象为:
拨码开关设置好待写入数据 8'h41 (0100_0001),rw_ctrl的拨码开关打向0,按下按钮发送启动信号,向从设备写入数据。接着 rw_ctrl 的拨码开关打向1,再次按下按钮发送启动信号,从从设备读取数,此时LED被立刻点亮,点亮情况为 8'h41 (0100_0001),说明模块功能成功实现了对 ov5640 寄存器的读写操作。
下面对ILA抓取到的波形进行分析:
(1)写数据
向 ov5640(设备地址0111_100)地址为 16'h3820 (0011_1000_0010_0000) 的寄存器写入数据 8'h41 (0100_0001)。可以看到sda数据正确传输,同时从设备虽然不需要应答但也正常进行接收应答。(应答位的毛刺个人认为是从设备的应答信号持续时间不够长导致,从设备发送应答后开始接收主设备发送的数据,sda保持高阻态,因此主设备这边接收到了高电平。但并不影响i2c功能,因为sda数据是在scl高电平中点进行接收)
(2)读数据
从 ov5640(设备地址0111_100)地址为 16'h3820 (0011_1000_0010_0000) 的寄存器读取数据。发现主设备成功读取到了之前向从设备写入的数据 8'h41 (0100_0001)。
到此,所设计SCCB控制器功能验证结束,成功实现对ov5640 寄存器的读写操作。