特别篇:制作自己的MIPI屏幕转接板,以及在Hi3519DV500上驱动屏幕
所以说,人就不能把话说的太死,新一期的《海思觉迷录》里,又要和读者朋友们的老朋友见面了(笑),当然这个也跟近期笔者的各种个人事务和身体状态有关
不过这次 Hi3519 严格来说并不算主角,并且,这一期的内容因为涉及硬件,所以也比较特别
而且,也得让读者朋友们记起笔者勉强还算个嵌入式工程师,讲了这么多期软件,偶尔也得来点硬件相关的吧(笑)
众所周知,市面上售卖的各种排线屏幕,甭管协议是 RGB、MIPI 还是 BT1120,它们都有一个特点————接口引脚定义基本上就没统一过,它们往往是专门适配某个厂家的一种或几种特定的开发板(当然大部分都适配了树莓派)。要想将这些屏幕适配到其他的开发板上就需要费很大一番功夫。
很不幸,笔者手头的这块 ST7701S 的 2.8 寸屏幕就是这样,走的是 MIPI-DSI 协议。因为最近学校的事,需要把这个屏幕移植到那块 Hi3519DV500 上。

左图是屏幕正面,右图是背面的 PCB 板,上面还印刷了那个大 15Pin 接口的引脚定义(应该是树莓派 4 的)
另一个 22Pin 接口好像是适配树莓派 5 的?不过对笔者也没啥用处就是了
1. 转接板的绘制
1.1 准备工作
首先看一眼屏幕的接口定义和 Hi3519DV500 开发板上对应的屏幕输出接口定义吧:

这样就明确屏幕只要用到 2 lane 的 MIPI 和 I2C 就够了,屏幕一侧是 15Pin@1mm 的排线,开发板一侧则是 24Pin@0.5mm
其实 I2C 也没啥大用,这个是管触控功能的,不过我们这次不需要就是了
这时候有读者就要问了:屏幕端需要一个3.3V供电,可是 Hi3519 那头并没有对应的引脚啊? 那就从别的地方引一个吧:

现在又多了个 12Pin@0.5mm
这样一来,我们就明确了屏幕、转接板、Hi3519 开发板之间的连接关系了:

1.2 绘制工作
这次就用嘉立创 EDA 吧,主要是这次的板子不算太复杂,
而且笔者也不会用别的软件(笑)
首先是原理图,在正式绘制之前,确认好屏幕和开发板的引脚顺序、排线金手指的朝向以及要使用排线的同向/反向,这些决定了原理图的绘制以及选型
这里就不过多赘述了,读者朋友们直接看吧:

开发板一侧不需要的引脚就不做处理了
然后,这一期的重点来了这期神了:PCB 的绘制
因为这次涉及到高速信号的布线,所以有很多的注意事项
首先是器件的布局,宗旨就是在连线的时候尽可能少打过孔就可以了。
然后是板层,考虑到 MIPI 走线下方应该有完整且连续的地平面,笔者这里就直接无脑按照四层板处理了:顶层信号层--GND--GND--底层信号层。
接下来是阻抗规则,含 MIPI 布线的板子阻抗范围应保持在 100Ω±10% 的水准,这里建议直接用嘉立创的阻抗计算神器:

首先确定层压结构,然后根据选择的层压结构和需要的阻值来确定后面差分对的线宽和线距
线距一般不要超过线宽的两倍
板子到手之后,有条件的可以用万用表测量下 MIPI 差分对阻抗
现在开始正式布线了,首先是 MIPI 的布线,MIPI 的布线需要遵从差分对布线的规则。
根据刚才确定的线宽、线距信息,在嘉立创 EDA 中设置设计规则:

这里的线距和线宽就不大合适了
此外,差分对内等长误差尽可能控制在 5mil 之内,同样在设计规则中设置。

不但是差分对内要保持等长,差分对之间也要保持等长,不过这里规则相对较为宽松,一般在 50mil 之内即可。

然后是过孔,过孔数量正常最好控制在 2 个及以内,不要超过 4 个,在 MIPI 信号过孔的旁边一般要紧挨着放置地过孔。
最好让差分对内的 P 和 N 两条线使用相同数量的过孔,以保持电气路径的对称性。
另外,布线从起点到第一个过孔、过孔到过孔以及过孔到终点之间的部分也需要满足差分对内/差分对间等长原则。
另外,差分对之间、差分对与其他信号之间的间距,建议至少为 3 倍线宽。
在 FPC 连接器刚引出的部分,可能无法拉开 3 倍线宽
这种时候可以利用引脚定义中两组差分对之间的 GND 引脚,引出一段地线并连接过孔,打通到地层
在两组差分对之间,也可以沿着它们出去的方向,每隔一段距离继续放置地过孔,连接表层 GND 铜皮和内层地

关于 MIPI 部分的布线说完了,接下来说说其他部分,首先是电源线:
电源线要尽可能地远离 MIPI 布线(包括 FPC 插座的金属引脚),建议至少保持 3 倍线宽以上的距离,同时在布线时采用两侧包地:在电源线两侧,各拉一条 GND 线,并每隔一段距离打一个过孔,连接到 GND 板层。
另外,电源输入部分可以酌情加滤波电容。

最后是 I2C 线,这个就比较简单,只要尽可能地远离 MIPI 布线即可。
确认板子的 DRC 无误之后,就可以准备在嘉立创平台下单了
2. 屏幕驱动适配
硬件的问题解决了,接下来就到了写驱动代码的时候了。
2.1 准备工作
说到屏幕的话,我们这次有数据手册、树莓派的驱动源码和单独的初始化启动序列。
其实有其他平台的驱动源码的话,我们往往可以直接从源码找有效信息,而不用从头翻数据手册了
确认屏幕基本信息:
比如屏幕的接口类型(MIPI-DSI/CSI),工作需要的差分对组数(1/2/4lane),数据的输出模式(DSI-VIDEO/DSI-CMD),屏幕的分辨率/帧率,像素格式(YUV420/YUV422/RGB888),接口初始化时的工作模式(LP(低功耗)/HS(高速)),视频工作模式(Burst(突发模式)、Non-Burst with Sync Pulses(同步脉冲非突发)、Non-Burst with Sync Events(同步事件非突发)),屏幕在水平/垂直两个方向上的同步(有效区、前后消隐、同步时序等),像素时钟频率,以及屏幕支持输入数据的速率等。
可以使用海思提供的《RGB_MIPI屏幕时钟时序计算器》,通过前面已知的时序和帧率信息来计算像素时钟频率和数据速率
初始化启动序列:
屏幕厂家一般都会提供这个,要不然就得在数据手册里慢慢扒了......
开发板引脚功能配置:
我们需要配置开发板相关引脚的功能为 MIPI_TX,对相关寄存器做以下配置:
write_reg_val 0x10260078 0x1200 #DSI_D3N
write_reg_val 0x1026007C 0x1200 #DSI_D3P
write_reg_val 0x10260080 0x1200 #DSI_D1N
write_reg_val 0x10260084 0x1200 #DSI_D1P
write_reg_val 0x10260088 0x1200 #DSI_CKN
write_reg_val 0x1026008C 0x1200 #DSI_CKP
write_reg_val 0x10260090 0x1200 #DSI_D0N
write_reg_val 0x10260094 0x1200 #DSI_D0P
write_reg_val 0x10260098 0x1200 #DSI_D2N
write_reg_val 0x1026009C 0x1200 #DSI_D2P
这个
write_reg_val是笔者自制的工具,具体缘由详见这一期:https://www.cnblogs.com/Wintoki/articles/18859553
有 bspmm 工具的话,直接用 bspmm 工具即可
2.2 编写
接下来就是编写实际驱动内容了,首先是VO/MIPI相关的配置:
/* VO: USER 480x640_60, TX: USER 480x640 */
static sample_vo_mipi_tx_cfg g_vo_tx_cfg_480x640_user = {
.vo_config = {
.vo_dev = SAMPLE_VO_DEV_DHD0, // VO 设备号
.vo_layer = SAMPLE_VO_LAYER_VHD0, // VO 视频层
.vo_intf_type = OT_VO_INTF_MIPI, // VO 输出层
.intf_sync = OT_VO_OUT_USER, // 用户同步设置
.bg_color = COLOR_RGB_BLACK, // 背景色
.pix_format = OT_PIXEL_FORMAT_YVU_SEMIPLANAR_420, // 像素格式
.disp_rect = {0, 0, 480, 640}, // 显示区域
.image_size = {480, 640}, // 图像尺寸
.vo_part_mode = OT_VO_PARTITION_MODE_SINGLE, // 视频层分割模式
.dis_buf_len = 3, // 视频层显示缓存的长度
.dst_dynamic_range = OT_DYNAMIC_RANGE_SDR8, // 视频层输出动态范围类型
.vo_mode = VO_MODE_1MUX, // 多画面分割模式
.compress_mode = OT_COMPRESS_MODE_NONE, // 解压缩模式
.sync_info = {
0, 1, 0, // syncm iop intfb
640, 24, 20, // vact vbb vfb
480, 40, 30, 0, // hact hbb hfb hmid
0, 0, 0, // bvact bvbb bvfb
10, 4, // hpw vpw
0, 0, 0}, // idv ihs ivs (信号极性,0 为高有效)
// iop 为 1 表示逐行扫描
// intfb hmid bvact 等数据用于隔行扫描,现在的屏幕大多都是逐行扫描,这里没什么用(大概吧)
.user_sync = {
.manual_user_sync_info.user_sync_attr = {
.clk_src = OT_VO_CLK_SRC_PLL, // 时钟源
.vo_pll = {
/* 如果为 mipitx, 则根据像素时钟和分频模式设置(时钟计算器) */
.fb_div = 46, /* 46 fb div */
.frac = 0xFFFFFF,
.ref_div = 1, /* 1 ref div */
.post_div1 = 7, /* 7 post div1 */
.post_div2 = 7, /* 7 post div2 */
},
},
.manual_user_sync_info.pre_div = 1, // 前置分频,跟随时钟计算器设置
.manual_user_sync_info.dev_div = 1, // 设备分频,跟随时钟计算器设置
.clk_reverse_en = TD_FALSE, // 时钟相位反向
.op_mode = OT_OP_MODE_MANUAL, // 用户时序操作模式
},
.dev_frame_rate = SAMPLE_VO_DEV_FRAME_RATE, // 显示屏帧率,这里为 60
},
.tx_config = {
/* for combo dev config */
.intf_sync = OT_MIPI_TX_OUT_USER, // 跟随用户同步设置
/* for screen cmd */
.cmd_count = CMD_COUNT_480x640, // 启动序列命令数量
.cmd_info = g_cmd_info_480x640, // 启动序列内容
/* for user sync */
.combo_dev_cfg = {
.devno = 0, // 设备号
.lane_id = {0, -1, -1, -1}, // lane 设置,0 为开启
.out_mode = OUT_MODE_DSI_VIDEO, // 输出模式
.out_format = OUT_FORMAT_RGB_24BIT, // 输出格式
.video_mode = BURST_MODE, // 视频工作模式
.sync_info = { // 跟随时钟计算器设置
.hpw = 10, /* 10 pixel */
.hbp = 30, /* 30 pixel */
.hact = 480, /* 480 pixel */
.hfp = 30, /* 30 pixel */
.vpw = 4, /* 4 line */
.vbp = 20, /* 20 line */
.vact = 640, /* 640 line */
.vfp = 20, /* 20 line */
},
.phy_data_rate = 542, /* 542 Mbps */ // 跟随时钟计算器设置
.pixel_clk = 22572, /* 22572 KHz */ // 跟随时钟计算器设置
},
},
};
然后是启动序列:
#define CMD_COUNT_480x640 36
#define MAX_MBUF_LEN 18
static td_u8 g_mBuf[CMD_COUNT_480x640][MAX_MBUF_LEN] = {
{0xFF, 0x77, 0x01, 0x00, 0x00, 0x13},
{0xEF, 0x08},
{0xFF, 0x77, 0x01, 0x00, 0x00, 0x10},
{0xC0, 0x4f, 0x00},
{0xC1, 0x10, 0x0c},
{0xC2, 0x07, 0x14},
{0xCC, 0x10},
{0xB0, 0x0a, 0x18, 0x1e, 0x12, 0x16, 0x0c, 0x0e, 0x0d, 0x0c, 0x29, 0x06, 0x14, 0x13, 0x29, 0x33, 0x1c},
{0xB1, 0x0a, 0x19, 0x21, 0x0a, 0x0c, 0x00, 0x0c, 0x03, 0x03, 0x23, 0x01, 0x0e, 0x0c, 0x27, 0x2b, 0x1c},
{0xFF, 0x77, 0x01, 0x00, 0x00, 0x11},
{0xB0, 0x5d},
{0xB1, 0x61},
{0xB2, 0x84},
{0xB3, 0x80},
{0xB5, 0x4d},
{0xB7, 0x85},
{0xB8, 0x20},
{0xC1, 0x78},
{0xC2, 0x78},
{0xD0, 0x88},
{0xE0, 0x00, 0x00, 0x02},
{0xE1, 0x06, 0xa0, 0x08, 0xa0, 0x05, 0xa0, 0x07, 0xa0, 0x00, 0x44, 0x44},
{0xE2, 0x20, 0x20, 0x44, 0x44, 0x96, 0xa0, 0x00, 0x00, 0x96, 0xa0, 0x00, 0x00},
{0xE3, 0x00, 0x00, 0x22, 0x22},
{0xE4, 0x44, 0x44},
{0xE5, 0x0d, 0x91, 0xa0, 0xa0, 0x0f, 0x93, 0xa0, 0xa0, 0x09, 0x8d, 0xa0, 0xa0, 0x0b, 0x8f, 0xa0, 0xa0},
{0xE6, 0x00, 0x00, 0x22, 0x22},
{0xE7, 0x44, 0x44},
{0xE8, 0x0c, 0x90, 0xa0, 0xa0, 0x0e, 0x92, 0xa0, 0xa0, 0x08, 0x8c, 0xa0, 0xa0, 0x0a, 0x8e, 0xa0, 0xa0},
{0xE9, 0x36, 0x00},
{0xEB, 0x00, 0x01, 0xe4, 0xe4, 0x44, 0x88, 0x40},
{0xED, 0xff, 0x45, 0x67, 0xfa, 0x01, 0x2b, 0xcf, 0xff, 0xff, 0xfc, 0xb2, 0x10, 0xaf, 0x76, 0x54, 0xff},
{0xEF, 0x10, 0x0d, 0x04, 0x08, 0x3f, 0x1f},
{0x11},
{0x29},
{0x35, 0x00}};
static mipi_tx_cmd_info g_cmd_info_480x640[CMD_COUNT_480x640] =
{
/* {devno work_mode lp_clk_en data_type cmd_size cmd}, usleep_value */
/* 设备号 视频工作模式 低功耗时钟 指令类型 指令大小 指令内容 命令间隔时长 */
/* 关于指令类型: */
// 0x05: DCS短写,发送1字节的DCS命令,用于最基础的屏幕控制
// 0x23: 通用短写,可携带2字节厂商自定义数据,用于传输特定寄存器配置
// 0x29: 通用长写,用于传输大量数据(超3字节)的场景
// 使用两种短写时,实际指令内容按照小端序直接写在指令大小中,指令内容那一部分默认为 NULL
{{0, 0, 0, 0x29, 0x0006, g_mBuf[0]}, USLEEP_1000},
{{0, 0, 0, 0x23, 0x08EF, NULL}, USLEEP_1000},
{{0, 0, 0, 0x29, 0x0006, g_mBuf[2]}, USLEEP_1000},
{{0, 0, 0, 0x29, 0x0003, g_mBuf[3]}, USLEEP_1000},
{{0, 0, 0, 0x29, 0x0003, g_mBuf[4]}, USLEEP_1000},
{{0, 0, 0, 0x29, 0x0003, g_mBuf[5]}, USLEEP_1000},
{{0, 0, 0, 0x23, 0x10CC, NULL}, USLEEP_1000},
{{0, 0, 0, 0x29, 0x0011, g_mBuf[7]}, USLEEP_1000},
{{0, 0, 0, 0x29, 0x0011, g_mBuf[8]}, USLEEP_1000},
{{0, 0, 0, 0x29, 0x0006, g_mBuf[9]}, USLEEP_1000},
{{0, 0, 0, 0x23, 0x5db0, NULL}, USLEEP_1000},
{{0, 0, 0, 0x23, 0x61b1, NULL}, USLEEP_1000},
{{0, 0, 0, 0x23, 0x84b2, NULL}, USLEEP_1000},
{{0, 0, 0, 0x23, 0x80b3, NULL}, USLEEP_1000},
{{0, 0, 0, 0x23, 0x4db5, NULL}, USLEEP_1000},
{{0, 0, 0, 0x23, 0x85b7, NULL}, USLEEP_1000},
{{0, 0, 0, 0x23, 0x20b8, NULL}, USLEEP_1000},
{{0, 0, 0, 0x23, 0x78c1, NULL}, USLEEP_1000},
{{0, 0, 0, 0x23, 0x78c2, NULL}, USLEEP_1000},
{{0, 0, 0, 0x23, 0x88d0, NULL}, USLEEP_1000},
{{0, 0, 0, 0x29, 0x0004, g_mBuf[20]}, USLEEP_1000},
{{0, 0, 0, 0x29, 0x000c, g_mBuf[21]}, USLEEP_1000},
{{0, 0, 0, 0x29, 0x000d, g_mBuf[22]}, USLEEP_1000},
{{0, 0, 0, 0x29, 0x0005, g_mBuf[23]}, USLEEP_1000},
{{0, 0, 0, 0x29, 0x0003, g_mBuf[24]}, USLEEP_1000},
{{0, 0, 0, 0x29, 0x0011, g_mBuf[25]}, USLEEP_1000},
{{0, 0, 0, 0x29, 0x0005, g_mBuf[26]}, USLEEP_1000},
{{0, 0, 0, 0x29, 0x0003, g_mBuf[27]}, USLEEP_1000},
{{0, 0, 0, 0x29, 0x0011, g_mBuf[28]}, USLEEP_1000},
{{0, 0, 0, 0x29, 0x0003, g_mBuf[29]}, USLEEP_1000},
{{0, 0, 0, 0x29, 0x0008, g_mBuf[30]}, USLEEP_1000},
{{0, 0, 0, 0x29, 0x0011, g_mBuf[31]}, USLEEP_1000},
{{0, 0, 0, 0x29, 0x0007, g_mBuf[32]}, USLEEP_1000},
{{0, 0, 0, 0x05, 0x0011, NULL}, USLEEP_120000},
{{0, 0, 0, 0x05, 0x0029, NULL}, USLEEP_10000},
{{0, 0, 0, 0x23, 0x0035, NULL}, USLEEP_10000}};
然后仿照 sample_vio 的例程编写流程即可,注意把 vo 启动换成类似这样的形式:
static td_s32 sample_vio_start_vo(sample_vo_mipi_tx_cfg *vo_tx_cfg, sample_vo_mode vo_mode)
{
td_s32 ret;
const sample_vo_cfg *vo_config = &vo_tx_cfg->vo_config;
const sample_mipi_tx_config *tx_config = &vo_tx_cfg->tx_config;
vo_tx_cfg->vo_config.vo_mode = vo_mode;
ret = sample_comm_vo_start_vo(vo_config);
if (ret != TD_SUCCESS)
{
sample_print("start vo failed with 0x%x!\n", ret);
return ret;
}
sample_print("start vo dhd%d.\n", vo_config->vo_dev);
if ((vo_config->vo_intf_type == OT_VO_INTF_MIPI) || (vo_config->vo_intf_type == OT_VO_INTF_MIPI_SLAVE))
{
ret = sample_comm_start_mipi_tx(tx_config);
if (ret != TD_SUCCESS)
{
sample_print("start mipi tx failed with 0x%x!\n", ret);
return ret;
}
}
return ret;
}
这样就可以了
2.3 测试
测试效果如下:

项目开源仓库:https://gitee.com/Mr_tsura/st7701s-adapter-board-with-drv-hi3519dv500
立创开源广场(仅PCB):https://oshwhub.com/wintoki/hi3519dv500_mipi_dsi_converter_2


浙公网安备 33010602011771号