I.MX6U 裸机开发22. LCDID读取及IO初始化
- 一、实验内容
- 1. 读屏 ID原理
- 1.1 硬件上 ID 编码规则
- 1.2 模拟开关作用
- 1.3 为什么是 DATA23 / 15 / 7?为什么是 GPIO3 的 28 / 20 / 12?
- 1.4 电平转成 `0x4384`
- 1.5 读取步骤(对应代码)
- 2. 支持的屏幕 ID
- 3. 程序流程
- 4. 预期串口输出
- 二、调试方式(与前课不同)
- 虚拟机 Ubuntu 改成 WSL
- 1. 编译(WSL)
- 2. USB-OTG 下载(本课主用,Windows)
- 3. 串口查看日志
- 4. (可选)仍用 SD 卡
- 5. 故障排查
- 三、读取 ID 参考代码
本课在板子上读取正点原子 LCD 的型号 ID,并通过串口printf打印结果。
调试方式与前面课程不同:本课将使用 Windows 下 USB-OTG(HID SDP)下载到内存运行,不再依赖 SD 卡烧录。
名词速览(本课首次集中用到)
- USB-OTG(On-The-Go):板子上那路可当「从设备」连电脑的 USB;本课用它做下载,不是串口那根线。
- HID(Human Interface Device):一种 USB 设备类,常见于键鼠;i.MX 下载模式也伪装成 HID,方便免装专用驱动。
- SDP(Serial Download Protocol):NXP 规定的「串行下载协议」,规定主机如何把镜像写进芯片内存并跳转执行。
- DDR:板载大容量动态内存,程序下载后在这里运行;掉电/按 RESET 后内容丢失(USB 下载模式尤其如此)。
⭐ 本次验证目标:USB 下载启动功能
本课核心目的是验证USB OTG 下载 → DDR 内存运行这条链路是否可用,LCD ID 读取只是验证时的观察输出。验证成功的判定标准:
- 拨码
0100(USB 下载模式)后,设备管理器出现SE Blank 6ULL(VID_15A2 & PID_0080)
- SE Blank 6ULL:芯片处于「空白片 / 串行下载」状态时,Windows 看到的产品名。
- VID / PID:USB 厂商号 / 产品号;
15A2是 Freescale/NXP,0080对应 i.MX6ULL 下载模式。dl.py三步全部 OK:DCD_WRITE→WRITE_FILE→JUMP_ADDRESS(见下文调试节解释)- 程序在 DDR 中跑起来:串口打印
LCD ID = 0x4384(ATK4384),LED0 每 500ms 闪烁三条都满足,即说明 USB 下载启动功能验证通过;之后其他课程也可复用此方式快速调试,无需反复烧 SD 卡。
一、实验内容
1. 读屏 ID原理
先澄清一点:本课读的不是液晶控制器芯片内部的「厂商 ID 寄存器」,也没有对屏幕发 RGB/SPI 读命令。
正点原子的做法是:在屏幕模组背面用M0 / M1 / M2 三个电阻焊点预先焊成不同的高低电平组合,相当于给每款屏打了一个硬件「型号跳线」;MCU 上电后用 GPIO 把这 3 位读出来,再查表映射成软件里的ATK4384这类 ID。
不同尺寸/分辨率的屏,LCDIF 时序、像素时钟、分辨率都不一样。若写死一套参数,换一块屏就会花屏或点不亮。所以要先识别「接的是哪一块」,后面初始化才选对参数。
1.1 硬件上 ID 编码规则
在开发板屏幕背面,可以看到丝印和 M0、M1、M2 的电阻焊点:
每一款屏出厂时,M0~M2 上会选择性地焊接上拉或下拉电阻(有的脚接高、有的脚接低),形成 3 bit 组合。
MCU 把对应引脚配成输入后,读到的就是这 3 个电平:
| M2 | M1 | M0 | idx(软件拼出来的数) | 对应型号(本工程约定) |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | ATK4342 |
| 0 | 0 | 1 | 1 | ATK7084 |
| 0 | 1 | 0 | 2 | ATK7016 |
| 1 | 0 | 0 | 4 | ATK4384(本板实测) |
| 1 | 0 | 1 | 5 | ATK1018 |
所以「能读 ID」的本质是:读 3 个 GPIO 的高低电平,不是访问某个神秘的内存地址。
1.2 模拟开关作用
RGB 屏正常工作时,LCD_DATA0~23要传像素数据。若 ID 电阻网络一直挂在 DATA 线上,会干扰显示。
正点原子的做法是:ID 信号走LCD_DATAxS一侧,真正进 MCU 的是LCD_DATAx;中间用模拟开关(SGM)决定是否把「屏上的 M0~M2」接到 MCU。
控制脚接到LCD_VSYNC(原理图里的 SGM CTRL):
- 读 ID 前:把 VSYNC 配成 GPIO输出高→ 开关导通 → MCU 才能采到 M0~M2
- 平时显示:开关断开(或不走这条读 ID 路径),DATA 线专心传像素
一句话:VSYNC 在这里临时当「开关使能」用,不是在读场同步波形。
1.3 为什么是 DATA23 / 15 / 7?为什么是 GPIO3 的 28 / 20 / 12?
这些不是软件随便选的「地址」,而是原理图已经焊死的连线,再对照芯片手册的引脚复用表(IOMUXC)查出来的:
| 屏上丝印 | 接到 MCU 的 LCD 信号 | RGB 含义 | IOMUXC 宏(切到 GPIO) | 实际 GPIO |
|---|---|---|---|---|
| M0 | LCD_DATA23 | R7(红最高位) | IOMUXC_LCD_DATA23_GPIO3_IO28 | GPIO3_IO28 |
| M1 | LCD_DATA15 | G7(绿最高位) | IOMUXC_LCD_DATA15_GPIO3_IO20 | GPIO3_IO20 |
| M2 | LCD_DATA07 | B7(蓝最高位) | IOMUXC_LCD_DATA07_GPIO3_IO12 | GPIO3_IO12 |
| 开关控制 | LCD_VSYNC | 行同步 | IOMUXC_LCD_VSYNC_GPIO3_IO03 | GPIO3_IO03 |
为何挑 R7/G7/B7(DATA23/15/7)这三根?
- 硬件设计如此:正点原子把 M0/M1/M2 复用到 RGB 总线最高位这三根线上,读完 ID 后同一组脚仍可改回 LCDIF 功能去显示。
- 软件侧只能跟着原理图走:你在代码里写
gpio_pinread(GPIO3, 28),是因为手册写明「焊盘 LCD_DATA23 复用到 GPIO 时 = GPIO3 的 bit28」;换成别的脚号,读到的就不是 M0。 - 「GPIO3_IO28」不是内存地址:它表示「GPIO 控制器 3 的第 28 号引脚」。底层会去操作该控制器的 DR/PSR 等寄存器,但你选 28/20/12 的依据是引脚编号,不是随便填的十六进制外设基址。
代码里两步缺一不可:
IOMUXC_SetPinMux(..._GPIO3_IOxx, 0):把焊盘从 LCDIF 功能切到 GPIOgpio_init+gpio_pinread:按输入方式读电平
若只配 GPIO 方向、不改 IOMUXC,脚仍接在 LCD 控制器上,读 ID 会失败。
1.4 电平转成0x4384
idx = M0 | (M1 << 1) | (M2 << 2)即:M0 为 bit0,M1 为 bit1,M2 为 bit2。例如本板 ATK4384:
- 读到 M0=0, M1=0, M2=1 →
idx = 0b100 = 4 if (idx == 4) return ATK4384; /* 0x4384 */
0x4384本身只是正点原子/本工程约定的型号常量,方便后面switch (lcdid)选分辨率;硬件上真正有信息量的是那 3 bit 的idx。
1.5 读取步骤(对应代码)
LCD_VSYNC→ GPIO 输出高,打开模拟开关LCD_DATA23/15/7→ GPIO 输入(必要时配上下拉,与电阻网络匹配)- 读 GPIO3_IO28 / 20 / 12,拼出
idx - 查表返回
ATKxxxx
配置引脚时会用到IOMUXC(IO Multiplexer Controller,引脚复用控制器):同一焊盘可切到 LCD 功能或 GPIO 功能,读 ID 时要切到 GPIO。
2. 支持的屏幕 ID
| idx | ID | 型号 | 规格 |
|---|---|---|---|
| 0 | 0x4342 | ATK4342 | 4.3 寸 480×272 |
| 1 | 0x7084 | ATK7084 | 7 寸 800×480 |
| 2 | 0x7016 | ATK7016 | 7 寸 1024×600 |
| 4 | 0x4384 | ATK4384 | 4.3 寸 800×480 |
| 5 | 0x1018 | ATK1018 | 10.1 寸 1280×800 |
表里的ID是软件约定的型号编码(不是面板内部寄存器里读出来的厂商 ID);后续初始化 LCD 时序、分辨率时,就靠这个值选参数。
3. 程序流程
上电 / USB 下载跳转 ├─ bsp_int_init() ├─ imx6u_clkinit() / clk_enable() ├─ led_init() / beep_init() / gpt1_delay_init() ├─ uart_init(UART1) # 115200 ├─ lcd_init() # 读 ID 并 printf └─ while(1) LED0 每 500ms 翻转关键代码:
board/imx6u/bsp_lcd.c—lcd_read_id()/lcd_init()board/imx6u/bsp_uart.c— UART1(异步串口,接 CH340 后变成电脑上的 COM 口)lib/stdio/lib/printf.c— 裸机 printf(没有操作系统标准库,自己往 UART 丢字符)bootloader/start.S— 启动汇编(设栈、清 BSS、跳进main)
4. 预期串口输出
本板使用正点原子 4.3 寸 800×480 屏(ATK4384),串口输出类似:
LCD ID = 0x4384(具体值取决于所接屏幕;本板实测为 ATK4384。)
二、调试方式(与前课不同)
| 对比项 | 前几课(常见) | 本课 |
|---|---|---|
| 编译环境 | 虚拟机里的 Ubuntu | WSL(Windows 子系统里的 Linux)交叉编译 |
| 下载介质 | SD 卡 +imxdownload(多在虚拟机里操作) | USB OTG + Windows 侧tools/dl.py |
| 运行位置 | 从 SD 启动 | 下载到DDR 内存后跳转(掉电丢失) |
| USB / 串口 | 虚拟机可做 USB 透传后再在 Ubuntu 里用 | 在 Windows 本机操作(本仓库 WSL1 看不到 USB) |
| 拨码 | SD 启动 | USB 下载模式0100 |
拨码 / BOOT_MODE:板子上的拨码开关告诉 BootROM「从哪启动」。0100表示进入Serial Downloader(串行下载器),只等电脑通过 USB 送程序,不会去读 SD。
虚拟机 Ubuntu 改成 WSL
前几课习惯在VMware / VirtualBox 里装 Ubuntu:交叉编译、imxdownload烧 SD、有时再把 USB 设备透传进虚拟机。能用,但有几处别扭:
- 工程在 Windows 盘与虚拟机磁盘之间来回拷,或要配共享文件夹
- USB 透传偶发不稳定(拔插、占用冲突)
- 虚拟机本身占内存、开机慢
本课起改用WSL(Windows Subsystem for Linux):在 Windows 里直接跑一套 Ubuntu 用户态,用来装arm-linux-gnueabihf-gcc做交叉编译。代码就放在 Windows 路径(如D:\documents\...),WSL 里用/mnt/d/...访问同一份文件,编译产物两边立刻可见,不必再拷来拷去。
本仓库当前是WSL1:
- 优点:和 Windows 文件系统集成紧、访问
D:盘快 - 限制:看不到 USB 设备(没有真正的 Linux USB 协议栈),所以
- 编译:仍在 WSL
- USB 下载(
dl.py)、串口助手(COM7):必须在Windows完成
若仍想用虚拟机,只要工具链一致、能生成同样的load.imx即可;本课文档按 WSL + Windows 下载来写。
1. 编译(WSL)
cd/mnt/d/documents/learn_i.mx6u/22_lcdidmakecleanmake产物(生成在工程目录,Windows 资源管理器里也能直接看到):
uart_printf.bin— 裸机二进制(纯机器码,还不能直接给 BootROM 用)load.imx— 带IVT + DCD头的 NXP 镜像(USB 下载 / SD 烧录都用它)
补充两个头信息名词:
- IVT(Image Vector Table,镜像向量表):文件最前面一块结构,告诉 BootROM「程序入口在哪、DCD 在哪、镜像自身加载地址(self)是多少」。
- DCD(Device Configuration Data):一串「写寄存器」命令,主要用来在跳进程序前初始化 DDR。没有 DCD,DDR 还没起来,大程序根本写不进去。
2. USB-OTG 下载(本课主用,Windows)
硬件准备:
- 拨码拨到USB 下载模式(正点原子:
0100,BOOT_MODE=10) - USB 线接板子OTG Type-C(不要插成串口那个口)
- 另一根线接USB 转串口 Type-C(板上是CH340芯片,把 UART 转成电脑 COM 口;本机为COM7)
- 设备管理器应出现 HID:
VID_15A2 & PID_0080,描述SE Blank 6ULL
依赖:
pip install hidapi pyserial下载并可选读串口:
# 脚本默认下载 22_lcdid\load.imx,并尝试读 COM7python D:\documents\learn_i.mx6u\tools\dl.py# 已用串口助手占用 COM7 时,只下载:python D:\documents\learn_i.mx6u\tools\dl.py--no-serial# 指定镜像 / 串口:python D:\documents\learn_i.mx6u\tools\dl.py D:\documents\learn_i.mx6u\22_lcdid\load.imx--com COM7脚本做的事(走SDP over HID,即用 HID 报告封装 SDP 命令):
DCD_WRITE:把 DCD 数据先写到OCRAM(片上 SRAM,地址0x00910000)。
DDR 还没初始化时不能往 DDR 写;OCRAM 上电即可用,BootROM 在这里执行 DCD 去「点亮」DDR。WRITE_FILE:DDR 就绪后,把整份load.imx写到 IVT 里的self地址(本课为0x877FF400,在 DDR 里)。JUMP_ADDRESS:让 BootROM 跳到 IVT,再按 IVT 里的entry进入你的程序。
成功时终端类似:
[1] DCD_WRITE ... status=0x128A8A12 [OK] [2] WRITE_FILE ... status=0x88888888 [OK] [3] JUMP_ADDRESS ... [OK] LCD ID = 0x4384失败后请按板子RESET,重新出现 “SE Blank 6ULL” 后再下载。
提醒:USB 下载模式下按 RESET = 重新进下载器,不会自动再跑刚才 DDR 里的程序;要看串口输出必须再执行一次dl.py。
3. 串口查看日志
| 参数 | 值 |
|---|---|
| 端口 | COM7(以本机设备管理器为准) |
| 波特率 | 115200 |
| 数据位 / 停止位 / 校验 | 8 / 1 / 无 |
波特率:每秒传输的符号速率;双方必须一致,否则就是乱码。本课 UART1 配成 115200 8N1(8 数据位、无校验、1 停止位)。
注意:
printf只在启动时打一次,先开串口再下载,或用dl.py自动读- 两个 Type-C:OTG = 下载,CH340 = 日志,插反会失败
- COM7 被 sscom 等占用时,
dl.py打不开串口,用--no-serial,日志改在串口助手看
4. (可选)仍用 SD 卡
与前课相同,适合需要掉电保存时:
cd/mnt/d/documents/learn_i.mx6u/22_lcdid../tools/imxdownload uart_printf.bin /dev/sdX# 先用 lsblk 确认设备号拨码改回 SD 启动后上电。此时程序在 SD 里,按 RESET 会重新启动应用,串口会再次打印 LCD ID——这点和 USB 下载模式相反。
5. 故障排查
下载后 ├─ LED0 闪? │ ├─ 是 → 查串口口/COM/波特率/是否错过启动打印 │ └─ 否 → 程序没跑起来 │ ├─ 无 SE Blank 6ULL → 拨码、OTG 线、按 RESET │ └─ 有设备但脚本失败 → 按 RESET 后重跑 dl.py └─ 串口乱码 → 波特率是否 115200三、读取 ID 参考代码
下面用到的IOMUXC_SetPinMux/IOMUXC_SetPinConfig就是在配引脚复用和电气特性(上下拉、驱动强度等);gpio_init/gpio_pinread则是把脚当普通输入输出用。
/** * 正点原子 LCD:M0~M2 组合 * LCD_DATA23=R7[M0]; LCD_DATA15=G7[M1]; LCD_DATA7=B7[M2] */unsignedshortlcd_read_id(void){unsignedcharidx=0;gpio_pin_config_tlcdid_io;IOMUXC_SetPinMux(IOMUXC_LCD_VSYNC_GPIO3_IO03,0);IOMUXC_SetPinConfig(IOMUXC_LCD_VSYNC_GPIO3_IO03,0x10B0);lcdid_io.direction=kGPIO_DigitalOutput;lcdid_io.outputLogic=1;/* 打开模拟开关 */gpio_init(GPIO3,3,&lcdid_io);IOMUXC_SetPinMux(IOMUXC_LCD_DATA07_GPIO3_IO12,0);IOMUXC_SetPinConfig(IOMUXC_LCD_DATA07_GPIO3_IO12,0xF080);IOMUXC_SetPinMux(IOMUXC_LCD_DATA15_GPIO3_IO20,0);IOMUXC_SetPinConfig(IOMUXC_LCD_DATA15_GPIO3_IO20,0xF080);IOMUXC_SetPinMux(IOMUXC_LCD_DATA23_GPIO3_IO28,0);IOMUXC_SetPinConfig(IOMUXC_LCD_DATA23_GPIO3_IO28,0xF080);lcdid_io.direction=kGPIO_DigitalInput;gpio_init(GPIO3,12,&lcdid_io);gpio_init(GPIO3,20,&lcdid_io);gpio_init(GPIO3,28,&lcdid_io);idx=(unsignedchar)gpio_pinread(GPIO3,28);idx|=(unsignedchar)gpio_pinread(GPIO3,20)<<1;idx|=(unsignedchar)gpio_pinread(GPIO3,12)<<2;if(idx==0)returnATK4342;if(idx==1)returnATK7084;if(idx==2)returnATK7016;if(idx==4)returnATK4384;if(idx==5)returnATK1018;return0;}