bsp学习—串口调试助手

1. 学习内容

  1. QT 图形化
  2. QT 和底层交互
  3. IMX6ULL(正点原子) 显示驱动移植
  4. IMX6ULL(正点原子) 触摸驱动移植

2. 学习到的知识库

2.1 QT 插件

2.1.1 Qt 在嵌入式 Linux 中的作用

Qt 应用不直接操作 LCD 或 GT911. Qt 通过 QPA 平台插件负责显示, 通过 generic input plugin 负责输入:

1
2
3
4
5
QApplication / QWidget
-> linuxfb: /dev/fb0
-> eglfs: DRM/EGL
-> evdevtouch: /dev/input/eventN
-> tslib: tslib + /dev/input/eventN

本项目是 Qt Widgets 应用. 正点原子 i.MX6ULL 无桌面环境时通常使用 linuxfb. 如果 BSP 使用 DRM/KMS 和 EGL, 应改用 eglfs.

2.1.2 Qt 插件和依赖部署

目标板至少需要 QtCore, QtGui, QtWidgets, 平台插件, 输入插件, 字体:

1
2
3
4
5
6
/usr/lib/libQt5Core.so.5 # QT 核心动态链接库
/usr/lib/libQt5Gui.so.5 # QT 图形基础动态链接库
/usr/lib/libQt5Widgets.so.5 # QT 控件库动态链接库
/usr/lib/plugins/platforms/libqlinuxfb.so # fb 的插件库
/usr/lib/plugins/generic/libqevdevtouchplugin.so # evdev 的插件库
/usr/share/fonts/ttf/SourceHanSansCN-Regular.otf # 字体

检查动态库和插件:

1
2
ldd /usr/bin/serial_assistant
QT_DEBUG_PLUGINS=1 /usr/bin/serial_assistant

QT_DEBUG_PLUGINS 开启插件调试功能

2.1.3 Qt 启动环境

1
2
3
4
5
6
7
export XDG_RUNTIME_DIR=/tmp/runtime-root # 指定为当前应用分配运行级的临时目录
mkdir -p "$XDG_RUNTIME_DIR"
chmod 700 "$XDG_RUNTIME_DIR"
export QT_QPA_PLATFORM=linuxfb:fb=/dev/fb0 # 指定 QT 的后端
export QT_QPA_FONTDIR=/usr/share/fonts/ttf # 指定 QT 读取的目录
export QT_QPA_GENERIC_PLUGINS=evdevtouch:/dev/input/eventN # 指定 QT 使用内核原生的电容触摸屏驱动
/usr/bin/serial_assistant

eventN 只是示例编号, 必须从 /proc/bus/input/devices 中找到 Goodix 对应的实际编号.

2.1.4 Qt 和底层交互

serial_assistant 不使用 Qt SerialPort. SerialPortBackend 通过 POSIX open, read, write 和 termios 访问串口, 并用 QSocketNotifier 把文件描述符事件接入 Qt 事件循环:

1
2
3
4
5
6
QSocketNotifier activated
-> SerialPortBackend::readAvailable()
-> dataReceived(QByteArray)
-> MainWindow::handleReceived()
-> FormatUtils::bytesToDisplay()
-> 接收区 QWidget

2.1.5 三篇内容中的 Qt 控件

本项目的三个 Qt/BSP 主题分别对应界面构建, 底层事件接入以及显示和触摸部署. 实际创建的界面控件集中在 MainWindow, 显示和触摸篇不新增专用控件, 而是通过 Qt QPA 把 framebuffer 和 Linux input 事件交给已有控件.

主题 Qt 控件或类 在本项目中的用途
Qt 图形化 QMainWindow, QWidget 创建主窗口和中央部件, 承载全部界面内容
Qt 图形化 QGroupBox 分组显示串口设置, 接收区和发送区
Qt 图形化 QLabel 显示字段名称, 连接状态和收发字节计数
Qt 图形化 QComboBox 选择端口, 波特率, 数据位, 校验, 停止位和行尾
Qt 图形化 QPushButton 刷新, 打开, 关闭, 清空, 发送和软键盘按键
Qt 图形化 QCheckBox 切换 HEX 显示, HEX 发送和自动滚动
Qt 图形化 QPlainTextEdit 显示接收数据和编辑待发送数据
Qt 图形化 QMessageBox 弹出打开串口和发送失败提示
Qt 图形化 QVBoxLayout, QHBoxLayout, QGridLayout 管理主窗口, 选项栏和触摸软键盘的布局
Qt 与底层交互 QObject, QSocketNotifier 以对象信号槽和文件描述符可读事件接入 Qt 事件循环, 这些不是可见控件
显示和触摸部署 QApplication, QWidget 及上述输入控件 linuxfb 或 eglfs 负责显示, evdevtouch 或 tslib 负责输入, GT911 不直接由控件访问

控件层级可以概括为:

1
2
3
4
5
6
7
8
9
10
11
12
QApplication
-> QMainWindow
-> QWidget (centralWidget)
-> QGroupBox (串口设置)
-> QLabel + QComboBox + QPushButton
-> QGroupBox (接收区)
-> QPlainTextEdit + QCheckBox + QPushButton
-> QGroupBox (发送区)
-> QPlainTextEdit + QCheckBox + QComboBox + QPushButton
-> QWidget (触摸软键盘)
-> QPushButton...
-> QLabel (状态和计数)

2.2 显示驱动移植

这里可以具体查看之前写过的显示屏移植篇

2.2.2 查找已有驱动

1
2
3
rg -n "panel|lcdif|mxcfb|backlight" \\
/path/to/linux/drivers \\
/path/to/linux/arch/arm/boot/dts

通用决策:

  1. 已有完全匹配的 panel 驱动, 只修改设备树.
  2. 已有通用 panel 驱动, 增加 compatible 和 display-timings.
  3. 没有匹配驱动或初始化时序不兼容, 才编写 panel 驱动.

panel: 屏幕面板驱动

2.2.3 配置内核

配置名称随 BSP 版本变化, 以当前 defconfig 和 make menuconfig 为准. i.MX6ULL 旧版 framebuffer BSP 常见选项:

1
2
3
4
CONFIG_FB=y
CONFIG_FB_MXC=y
CONFIG_BACKLIGHT_CLASS_DEVICE=y
CONFIG_FB_MODE_HELPERS=y

新版 DRM/KMS BSP 应配置对应的 DRM, LCDIF, panel 和 backlight 选项.

2.2.4 编写设备树

设备树要连接 LCDIF, pinctrl, reset, backlight, 电源和 display-timings. 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
&lcdif {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_lcdif_dat &pinctrl_lcdif_ctrl>;
status = "okay";

display0: display@0 {
compatible = "vendor,panel-model";
reset-gpios = <&gpio1 5 GPIO_ACTIVE_LOW>;
backlight = <&backlight>;
status = "okay";

display-timings {
native-mode = <&timing0>;
timing0: timing0 {
clock-frequency = <51200000>;
hactive = <1024>;
vactive = <600>;
hfront-porch = <160>;
hback-porch = <140>;
hsync-len = <20>;
vfront-porch = <12>;
vback-porch = <23>;
vsync-len = <3>;
};
};
};
};

backlight: backlight {
compatible = "gpio-backlight";
gpios = <&gpio1 8 GPIO_ACTIVE_HIGH>;
default-on;
};

时序数值必须替换为屏幕手册中的值. 引脚复用示例:

1
2
3
4
5
6
7
pinctrl_lcdif_dat: lcdifdatgrp {
fsl,pins = <
MX6UL_PAD_LCD_DATA00__LCDIF_DATA00 0x79
MX6UL_PAD_LCD_DATA01__LCDIF_DATA01 0x79
/* 其余 LCD_DATA 和控制引脚 */
>;
};

pad 宏和电气参数必须查询当前 SoC 的 pinfunc 头文件. 同一个 GPIO 不能同时被 regulator, LED, reset 或触摸节点占用.

2.2.5 编译, 烧录和验证

1
2
3
4
cd /path/to/linux
make ARCH=arm CROSS_COMPILE=/path/to/arm-linux-gnueabihf- olddefconfig
make ARCH=arm CROSS_COMPILE=/path/to/arm-linux-gnueabihf- zImage dtbs -j2
sha256sum arch/arm/boot/zImage arch/arm/boot/dts/imx6ull-board.dtb

启动目标板后先不要运行 Qt:

1
2
3
4
ls -l /dev/fb0
cat /proc/fb
dmesg | grep -i -E 'fb|lcd|lcdif|mxcfb|panel|backlight'
fbset -i 2>/dev/null

问题定位:

  • 没有 /dev/fb0, 查设备树 status, 内核配置, pinctrl 和 driver probe.
  • 有 /dev/fb0 但黑屏, 查背光, reset.
  • 有画面但偏色, 查 RGB 位宽.
  • 有画面但抖动, 查 pixel clock 极性, 时钟频率.
  • 分辨率不对, 查 U-Boot 参数.

确认 framebuffer 正常后再验证 Qt:

1
2
export QT_QPA_PLATFORM=linuxfb:fb=/dev/fb0
/usr/bin/serial_assistant

2.3 触摸驱动移植

2.3.1 确认 GT911 硬件

从触摸模组手册和原理图确认控制器型号, I2C 总线, 7-bit 地址, INT GPIO, RST GPIO, 中断触发方式, 触摸范围和坐标方向. GT911 常见地址为 0x5d 或 0x14, 地址由复位期间 INT 电平决定.

本项目链路:

1
GT911 -> I2C2 -> Goodix 内核驱动 -> Linux input -> /dev/input/eventN -> Qt

2.3.2 验证 I2C

1
2
3
i2cdetect -l
i2cdetect -y 1
dmesg | grep -i -E 'i2c|goodix|touch'

I2C communication failure: -5 通常发生在内核 probe 阶段.

2.3.3 设备树和内核驱动

优先启用内核已有的 goodix 驱动. GT911 DTS 示例:

goodix 驱动: 一个触摸屏驱动, 加载之后会在系统下生成 /dev/input/eventX 节点.
其底部汇总了 GT911, GT9271, GT9147 等芯片

1
2
3
4
5
6
7
8
9
10
11
12
13
&i2c2 {
goodix_ts: touchscreen@5d {
compatible = "goodix,gt911", "goodix,gt9xx";
reg = <0x5d>;
interrupt-parent = <&gpio1>;
interrupts = <9 IRQ_TYPE_NONE>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_ts_int &pinctrl_ts_reset>;
goodix,irq-gpio = <&gpio1 9 GPIO_ACTIVE_LOW>;
goodix,rst-gpio = <&gpio5 9 GPIO_ACTIVE_LOW>;
status = "okay";
};
};

逐句解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
&i2c2 {                                      /* 引用板级设备树中已经定义的 I2C2 控制器 */
goodix_ts: touchscreen@5d { /* 创建触摸屏节点, 标签 goodix_ts 供其他节点引用 */
compatible = "goodix,gt911", "goodix,gt9xx"; /* 优先匹配 GT911, 也兼容 Goodix GT9xx 通用驱动 */
reg = <0x5d>; /* I2C 从设备地址, 这里使用 7-bit 地址 0x5d */
interrupt-parent = <&gpio1>; /* 指定中断控制器为 GPIO1 */
interrupts = <9 IRQ_TYPE_NONE>; /* 使用 GPIO1_IO09 作为中断线, 触发类型以驱动或硬件为准 */
pinctrl-names = "default"; /* 声明默认 pinctrl 状态名称 */
pinctrl-0 = <&pinctrl_ts_int &pinctrl_ts_reset>; /* 应用触摸中断和复位引脚的复用及电气配置 */
goodix,irq-gpio = <&gpio1 9 GPIO_ACTIVE_LOW>; /* Goodix 驱动使用的中断 GPIO, 低电平有效 */
goodix,rst-gpio = <&gpio5 9 GPIO_ACTIVE_LOW>; /* Goodix 驱动使用的复位 GPIO, 低电平有效 */
status = "okay"; /* 启用该触摸设备节点, disabled 表示不加载 */
};
};

注意事项:

  1. touchscreen@5d 中的 @5d 应与 reg 的 I2C 地址一致, 节点名称本身不是驱动匹配条件.
  2. interrupts 的触发标志不能脱离当前内核的 GPIO binding 猜测, IRQ_TYPE_NONE 表示交由系统或驱动确定, 如果中断不工作应根据硬件电平改为上升沿, 下降沿或电平触发.
  3. pinctrl-0 中的 pinctrl_ts_int 和 pinctrl_ts_reset 必须在同一份设备树中定义, 并且对应引脚不能同时被 regulator, LED 或其他外设占用.
  4. reg 的地址必须根据 GT911 复位时序和硬件连接确认, 常见地址为 0x5d 或 0x14.

属性名必须以当前内核 Documentation/devicetree/bindings/input/touchscreen binding 为准. 本板 GPIO1_IO09 曾被 3.3 V regulator 使用, 必须删除冲突的 GPIO 属性.

2.3.4 验证 Linux input

1
2
3
4
5
cat /proc/bus/input/devices
ls -l /dev/input/event* # 本板上 event0 对应电源
dmesg | grep -i -E 'goodix|touch|input'
cat /proc/interrupts | grep -i -E 'goodix|gpio|touch'
od -An -tx1 -w16 /dev/input/eventN

确认 Goodix 对应的 eventN 后, 按下和移动触摸时 od 应出现新数据, 中断计数也应增加. 没有 event 节点说明 probe 或 input 注册失败. event 存在但无数据, 重点检查 INT, 排线和中断配置.

2.3.5 接入 Qt

Qt 不直接访问 GT911, 而是读取 Linux input event. 先使用 evdevtouch:

1
export QT_QPA_GENERIC_PLUGINS=evdevtouch:/dev/input/eventN # Qt 框架中用于显式指定通用输入设备插件

需要滤波和校准时使用 tslib:

tslib 是一个开源的嵌入式 linux 触摸屏数据校准框架

1
2
3
export TSLIB_TSDEVICE=/dev/input/eventN # TSlib 库绑定的触摸屏设备节点
export TSLIB_CONFFILE=/etc/ts.conf # TSlib 库的配置文件路径
export QT_QPA_GENERIC_PLUGINS=tslib:/dev/input/eventN # 告知 QT 加载插件

坐标偏移或方向错误时, 先确认内核上报的 X/Y 范围, 再检查屏幕旋转和 /etc/pointercal. 不要先修改应用按钮坐标.

References