简介
以下介绍的裸机开发均基于i.MX6 Platform SDK完成,最后的版本于2013年发布,在使用gcc9编译时会遇到一些小问题,这里不做具体介绍。所有的开发均在Ubuntu 20.04上完成。目标平台为i.MX6D/Q,大部分也适用于i.MX6S/DL,但是对于i.MX6UL/ULL会需要较大改动。
编译与下载
PlatformSDK的Makefile中已经包含了产生目标镜像的代码,直接编译即可产生可以烧写进SD卡或者通过uuu载入的镜像,SPI Flash有待研究。(uuu是代替了MfgTools的跨平台i.MX USB烧录工具,可支持i.MX6\7\8)
可以直接使用如sudo uuu application.bin
的命令下载程序至RAM运行。如果需要写成脚本,可以参考如下的写法:
uuu_version 1.2.135 SDP: boot -f path_to_the_application.bin SDP: done
如果需要使用JTAG下载,则需要单独编写初始化DDR内存的初始化脚本,暂未进行测试。
串口
在EVK上默认调试串口为UART4,如果实际使用的板子并非使用UART4,则需要修改成板子上对应使用的串口。定义位于board/common/hardware_modules.c中。
内存配置
DDR控制器配置位于启动镜像的DCD区域中。如果使用的内存与EVK不同,则通常需要修改DCD配置。这部分数据位于board/mx6dq/board/dcd.c中。可以考虑使用官方的Programming Aid工具和DDR Stress Test工具来产生合适的DDR控制器配置。
不同板子拥有不同的内存容量。通常会区分出一部分不可cache内存用作DMA使用。主内存映射代码位于sdk/core/src/mmu.c中,具体的DMA内存设定定义在apps/common/platform_init.c中。以下为我在1GB RAM板子上使用的内存分割方案,仅供参考:
mmu_map_l1_range(0x10000000, 0x10000000, 0x20000000, kOuterInner_WB_WA, share_attr, kRWAccess); // First 512MB
mmu_map_l1_range(0x30000000, 0x30000000, 0x20000000, kNoncacheable, kShareable, kRWAccess); // Last 512MB
HDMI输出
1280x720 60Hz
官方的HDMI输出例程在时钟配置上存在很大问题。首先虽然设置的为1080p60输出(像素时钟148.5MHz)但是,实际上PLL设置的输出频率为约75MHz,即720p60的频率。然而即使设定到720p60,依然无法正常输出,原因为官方PLL设定代码中存在笔误。
设定代码位于board/common/board_hdmi.c中。按照注释,官方代码中PLL3 PFD1设置为了445MHz,随后进行6分频,得到74.16MHz的频率,与74.25MHz很接近。然而实际上PLL3 PFD1设置的频率为454MHz,6分频后为75.6MHz,过高了。更好的设定为将PFD1设置为480MHz*18/29=298MHz,随后进行4分频得到74.48MHz。当然更好的做法是使用iMX6中专用的视频PLL来产生这个频率,而非使用调节范围有限的PLL3 PFD1。
以下为最终使用的配置:
void hdmi_clock_set(int ipu_index, uint32_t pclk)
{
switch (pclk) {
case 74250000:
if (ipu_index == 1) {
//clk output from 540M PFD1 of PLL3
HW_CCM_CHSCCDR.B.IPU1_DI0_CLK_SEL = 0; // derive clock from divided pre-muxed ipu1 di0 clock
HW_CCM_CHSCCDR.B.IPU1_DI0_PODF = 3; // div by 4
HW_CCM_CHSCCDR.B.IPU1_DI0_PRE_CLK_SEL = 5; // derive clock from 540M PFD
}
//config PFD1 of PLL3 to be 298MHz
BW_CCM_ANALOG_PFD_480_PFD1_FRAC(29);
break;
default:
printf("the hdmi pixel clock is not supported!\n");
}
}
1024x768 60Hz
由于其它例程均使用了XGA分辨率的LVDS输出,这里为了能够在HDMI上复现其它测试,给HDMI代码增加XGA模式支持。
首先在hdmi_test函数中, 需要选择使用DVI模式,使用DMT模式16:
myHDMI_info.video_mode->mCode = 16;
myHDMI_info.video_mode->mHdmiDviSel = 0;
随后给先前的PLL设定代码增加XGA使用的65MHz时钟产生,这里使用480MHz*18/19/7=64.96MHz时钟。
case 65000000:
if (ipu_index == 1) {
//clk output from 540M PFD1 of PLL3
HW_CCM_CHSCCDR.B.IPU1_DI0_CLK_SEL = 0; // derive clock from divided pre-muxed ipu1 di0 clock
HW_CCM_CHSCCDR.B.IPU1_DI0_PODF = 6; // div by 7
HW_CCM_CHSCCDR.B.IPU1_DI0_PRE_CLK_SEL = 5; // derive clock from 540M PFD
}
//config PFD1 of PLL3 to be 454MHz
BW_CCM_ANALOG_PFD_480_PFD1_FRAC(19);
break;
在IPU驱动中(sdk/drivers/ipu/src/ips_disp_panel.c)增加对于XGA 60的时序定义:
{
"HDMI XGA 60Hz", // name
HDMI_XGA60, // panel id flag
DISP_DEV_HDMI, // panel type
DCMAP_RGB888, // data format for panel
60, // refresh rate
1024, // panel width
768, //panel height
65000000, // pixel clock frequency
296, // hsync start width
136, // hsync width
24, // hsyn back width
32, // vysnc start width
3, // vsync width
6, // vsync back width
0, // delay from hsync to vsync
0, // interlaced mode
1, // clock selection, external
0, // clock polarity
1, // hsync polarity
1, // vync polarity
1, // drdy polarity
0, // data polarity
&hdmi_xga60_init,
&hdmi_xga60_deinit,
},
同样在HDMI驱动(sdk/drivers/hdmi/src/hdmi_common.c)中也需要加入对于DMT模式16的支持:
switch (vmode->mCode) {
case 16:
vmode->mHActive = 1024;
vmode->mVActive = 768;
vmode->mHBlanking = 320;
vmode->mVBlanking = 38;
vmode->mHSyncOffset = 24;
vmode->mVSyncOffset = 3;
vmode->mHSyncPulseWidth = 136;
vmode->mVSyncPulseWidth = 6;
vmode->mHSyncPolarity = vmode->mVSyncPolarity = FALSE;
vmode->mInterlaced = FALSE;
vmode->mPixelClock = 6500;
}
另外建议在HDMI PHY驱动(sdk/drivers/hdmi/src/hdmi_tx_phy.c)中也增加对于65MHz时钟的支持代码。默认配置也足以支持显示,目前尚不知正确的配置方法。暂且直接照搬了72MHz的初始化代码。
GPU
需要注意的是,GPU非常吃内存带宽,为此有必要设置总线的QoS以确保IPU能得到足够的内存带宽用于刷新屏幕。请自行根据实际使用的IPU进行设置:
//IPU QoS
HW_IOMUXC_GPR6_SET(0xFFFFFFFF); // IPU1 QoS
//HW_IOMUXC_GPR7_SET(0xFFFFFFFF); // IPU2 QoS
PCIe
这里只讨论PCIe用作EP模式的情况。RC模式可以直接参考原有的代码。
关于时钟
对于EP设备,EP使用的125MHz或者250MHz时钟应该从主机提供的100MHz参考时钟产生。然而i.MX内置的125MHz PLL似乎并没有办法支持外部100MHz时钟输入。可能需要外置PLL芯片。
内置的PLL同样无法满足Gen2 PCIe通信的时钟要求,如果需要Gen2 5GT/s的速度,则需要外置PLL芯片。
关于中断
PCIe在EP模式下,可以通过MSI从EP产生RC上的中断(https://community.nxp.com/thread/321747),但是并没有任何办法由RC产生EP上的中断(https://community.nxp.com/thread/312874)。通常而言,EP设备会有一个doorbell或者malibox寄存器触发设备读取寄存器内容进行操作。轮询是一种可行的做法。如果需要中断,一种思路为在BAR中映射一个与PCIE无关的外设,如定时器,让主机直接操作这个外设来产生EP设备(iMX6)上的中断。
关于EP寄存器
DW的PCIE EP控制器似乎是设计用于通用的可编程EP控制器的:在连接中断(如被主机复位)时,所有EP寄存器会被恢复为初始值,包括且不限于:VID PID、设备类、BAR配置等。在搭配PC使用时尤其需要注意,许多BIOS会复位PCIe链路,导致所有这些设置丢失。一个可行的做法是,等到PC启动到Grub后再启动iMX6 EP,建立链路完成初始化后再启动PC上的Linux系统,由Linux系统来分配这个设备的内存,而非由BIOS完成。同时已知BIOS会对设备进行一些不确定的操作导致iMX6 hang,为此在BIOS自检完成后再启动EP是有必要的。
关于BAR
BAR0到4并不是一样的。
BAR0和BAR1在一起构成64bit的BAR,类型为内存,可预取,mask 0xfffff,mask可写。在32位模式下BAR1也不能单独使用。
BAR2:32位寻址,类型为内存,可预取,mask 0xfffff,mask可写。
BAR3:32位寻址,类型为IO,mask 0xff。
以上为non-documented feature。似乎从试验结果来看,确实如此,并不能像参考手册一样随意设置BAR。
来源 https://community.nxp.com/thread/428633
除了可写的两个mask,其他均无法进行修改。即可以把BAR0设置为32bit,与此同时BAR1会被自动禁用。或者也可以作为64bit BAR使用。BAR2可以自由设置大小,但是只能为32bit模式。BAR3不能进行修改。
以下是来自Linux启动日志中的默认BAR配置:
[ 1.875727] pci 0000:04:00.0: reg 0x10: [mem 0xfaf00000-0xfaffffff 64bit pref] [ 1.883699] pci 0000:04:00.0: reg 0x18: [mem 0xfae00000-0xfaefffff pref] [ 1.891698] pci 0000:04:00.0: reg 0x1c: [io 0xe800-0xe8ff] [ 1.895718] pci 0000:04:00.0: reg 0x30: [mem 0xfbff0000-0xfbffffff pref] [ 1.903786] pci 0000:04:00.0: supports D1 [ 1.907687] pci 0000:04:00.0: PME# supported from D0 D1 D3hot D3cold [ 1.911725] pci 0000:04:00.0: 2.000 Gb/s available PCIe bandwidth, limited by 2.5 GT/s x1 link at 0000:00:1c.0 (capable of 4.000 Gb/s with 5 GT/s x1 link)
把BAR0设置为64KB不可预取的内存的代码:
HW_PCIE_EP_DEVICEID.U = 0xDEADBEAF;
HW_PCIE_RC_REVID.U |= (PCI_CLASS_MEMORY_RAM << 16);
HW_PCIE_EP_SSID.U = 0xDEADBEEF;
关于地址映射
对于EP而言,地址是由RC分配的。比如上图中BAR0被分配到了0xfaf00000的位置。显然对于iMX而言,BAR0应该是一块固定的本地地址。这个地址翻译由iATU完成。从RC向EP的读写为inbound mapping,而从EP向RC读写为outbound mapping。
对于Inbound mapping,iATU可以自己根据BAR得到的地址进行映射,而不需要由软件监视BAR寄存器再配置映射地址。以下函数便可以完成BAR0到指定地址的映射:
uint32_t pcie_map_inbound(uint32_t viewport, uint32_t tlp_type,
uint32_t addr_base_cpu_side, uint32_t bar)
{
// configure as an inbound region
HW_PCIE_PL_IATUVR_WR(BF_PCIE_PL_IATUVR_REGION_INDEX(viewport) |
BF_PCIE_PL_IATUVR_REGION_DIRECTION(PCIE_IATU_VP_DIR_INBOUND));
// configure target address
HW_PCIE_PL_IATURLTA_WR(addr_base_cpu_side);
HW_PCIE_PL_IATURUTA_WR(0);
// configure TLP type
HW_PCIE_PL_IATURC1_WR(BF_PCIE_PL_IATURC1_TYPE(tlp_type));
// enable region and bar match type
HW_PCIE_PL_IATURC2_WR(BF_PCIE_PL_IATURC2_BAR_NUMBER(0) |
BF_PCIE_PL_IATURC2_RESPONSE_CODE(0x0) |
BF_PCIE_PL_IATURC2_MATCH_MODE(1) |
BF_PCIE_PL_IATURC2_REGION_ENABLE(1));
return addr_base_cpu_side;
}
需要注意的是在pcie_common.h中对于PCIE_IATU_VP_DIR_INBOUND和PCIE_IATU_VP_DIR_OUTBOUND的定义反了。
初始化流程
1、启用PCIe使用的PLL(pcie_clk_setup) 2、以EP模式启用PCIe控制器(pcie_init) 3、写入PCIe配置(设备ID、子系统ID、设备类、BAR等) 4、启用PCIe链路 5、设置iATU地址映射
部分流程可交换。注意主机在整个过程完成后需要保证链路一直处于建立状态。