简介

以下介绍的裸机开发均基于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地址映射

部分流程可交换。注意主机在整个过程完成后需要保证链路一直处于建立状态。