Linux显示(五):QT显示插件(LinuxFB)及其依赖的驱动(DRM/Framebuffer)记录
关键词:Framebuffer、linuxfb、DRM等等。
基于《基于QEMU模拟器搭建Builtroot下的QT开发环境》搭建开发环境。
QT在Linux中支持多种显示插件,包括EGLFS、LinuxFB、DirectFB、Wayland等。可以通过--platfrom选项指定选择何种插件。比如:./analogclock --platform linuxfb。
QT支持多种显示插件,显示插件打开Linux内核fb设备,Linux内核中GPU/Display驱动将应用数据刷新到Display设备上。此处,简单记录以上过程涉及到的模块。
1. QT5中linuxfb显示插件
class QPlatformScreen作为基类派生了显示接口类:
src\plugins\platforms\android\qandroidplatformscreen.h: class QAndroidPlatformScreen: public QObject, public QPlatformScreen, public AndroidSurfaceClient src\plugins\platforms\cocoa\qcocoascreen.h: class QCocoaScreen : public QPlatformScreen src\plugins\platforms\directfb\qdirectfbscreen.h: class QDirectFbScreen : public QPlatformScreen src\plugins\platforms\eglfs\api\qeglfsscreen_p.h: class Q_EGLFS_EXPORT QEglFSScreen : public QPlatformScreen src\platformsupport\fbconvenience: class QFbScreen : public QObject, public QPlatformScreen src\plugins\platforms\haiku\qhaikuscreen.h: class QHaikuScreen : public QPlatformScreen src\plugins\platforms\ios\qiosscreen.h: class QIOSScreen : public QObject, public QPlatformScreen src\plugins\platforms\minimalegl\qminimaleglscreen.h: class QMinimalEglScreen : public QPlatformScreen src\plugins\platforms\minimal\qminimalintegration.h: class QMinimalScreen : public QPlatformScreen src\plugins\platforms\offscreen\qoffscreencommon.h: class QOffscreenScreen : public QPlatformScreen src\plugins\platforms\openwfd\qopenwfdscreen.h: class QOpenWFDScreen : public QPlatformScreen src\plugins\platforms\qnx\qqnxscreen.h: class QQnxScreen : public QObject, public QPlatformScreen src\plugins\platforms\wasm\qwasmscreen.h: class QWasmScreen : public QObject, public QPlatformScreen src\plugins\platforms\windows\qwindowsscreen.h: class QWindowsScreen : public QPlatformScreen src\plugins\platforms\winrt\qwinrtscreen.h: class QWinRTScreen : public QPlatformScreen src\plugins\platforms\xcb\qxcbscreen.h: class Q_XCB_EXPORT QXcbScreen : public QXcbObject, public QPlatformScreen
其中QFbScreen又派生了如下显示类:
src\plugins\platforms\bsdfb\qbsdfbscreen.h: class QBsdFbScreen : public QFbScreen src\plugins\platforms\integrity\qintegrityfbscreen.h: class QIntegrityFbScreen : public QFbScreen src\plugins\platforms\linuxfb\qlinuxfbdrmscreen.h: class QLinuxFbDrmScreen : public QFbScreen src\plugins\platforms\linuxfb\qlinuxfbscreen.h: class QLinuxFbScreen : public QFbScreen src\plugins\platforms\vnc\qvncscreen.h: class QVncScreen : public QFbScreen
通过make menuconfig进入Target packages->Graphic libraries and applications(graphic/text)->QT5配置显示插件,以及默认插件:
编译完成后在Target Rootfs中每个插件以库的形式保存:
/usr/lib/qt/plugins/platforms/ |-- libqeglfs.so |-- libqlinuxfb.so |-- libqminimal.so |-- libqminimalegl.so |-- libqoffscreen.so `-- libqvnc.so
1.1 选择platform插件
可通过设置环境变量QT_QPA_PLATFORM或者选项-platform指定使用哪个显示插件。
QGuiApplicationPrivate::createPlatformIntegration
->如果定义QT_QPA_DEFAULT_PLATFORM_NAME,则使用作为默认platform名称。
->qgetenv--获取QT_QPA_PLATFORM环境变量值。
->或者通过-platform指定platformName。
->init_platform
->QPlatformIntegrationFactory::create--加载platformName指定的插件。
2. QLinuxFbIntegrationPlugin插件
QPlatformIntegrationPlugin派生出QLinuxFbIntegrationPlugin,create()创建并返回QPlatformIntegration对象。
QPlatformIntegration* QLinuxFbIntegrationPlugin::create(const QString& system, const QStringList& paramList) { Q_UNUSED(paramList); if (!system.compare(QLatin1String("linuxfb"), Qt::CaseInsensitive)) return new QLinuxFbIntegration(paramList);--创建QLinuxFbIntegration()对象。 return 0; }
2.1 Framebuffer初始化
创建QLinuxFbIntegration对象时,主要创建了QFbScreen对象:
QLinuxFbIntegration::QLinuxFbIntegration(const QStringList ¶mList) : m_primaryScreen(nullptr), m_fontDb(new QGenericUnixFontDatabase), m_services(new QGenericUnixServices), m_kbdMgr(0) { #if QT_CONFIG(kms) if (qEnvironmentVariableIntValue("QT_QPA_FB_DRM") != 0) m_primaryScreen = new QLinuxFbDrmScreen(paramList);--如果是DRM,则创建DRM相关QFbScreen对象。 #endif if (!m_primaryScreen) m_primaryScreen = new QLinuxFbScreen(paramList);--如果是FB,则创建FB相关QFbScreen对象。 }
QFbScreen派生出QLinuxFbIntegration。QLinuxFbIntegration实现了:
- 构造函数创建Framebuffer句柄。
- 提供了对FB的初始化initialize(),打开Framebuffer并通过ioctl进行配置、mmap映射Framebuffer内存。
- grabWindow()抓取屏幕内容。
- doRedraw()刷新屏幕内存。
- event()如果产生需要更是屏幕的事件,则执行doRedraw()。
- 析构函数去内存映射,并关闭Framebuffer设备。
class QLinuxFbScreen : public QFbScreen { Q_OBJECT public: QLinuxFbScreen(const QStringList &args);--参数的初始化赋值。 ~QLinuxFbScreen();--去初始化,内存去映射以及关闭fb句柄。 bool initialize() override;--打开fb,获取显示相关参数,映射frame buffer到用户空间,并以frame buffer映射内存为缓存穿件QImage,最后清空屏幕。 QPixmap grabWindow(WId wid, int x, int y, int width, int height) const override;--通过QPixmap从映射QImage中获取屏幕内容。 QRegion doRedraw() override;--通过QPainter刷新frame buffer。 private: QStringList mArgs; int mFbFd; int mTtyFd; QImage mFbScreenImage; int mBytesPerLine; int mOldTtyMode; struct { uchar *data; int offset, size; } mMmap; QPainter *mBlitter; };
2.1.1 Framebuffer初始化
初始化插件时调用QLinuxFbIntegration::initialize()进行具体FB的初始化:
void QLinuxFbIntegration::initialize() { if (m_primaryScreen->initialize())--调用initialize()函数。 QWindowSystemInterface::handleScreenAdded(m_primaryScreen); else qWarning("linuxfb: Failed to initialize screen"); m_inputContext = QPlatformInputContextFactory::create(); m_vtHandler.reset(new QFbVtHandler); if (!qEnvironmentVariableIntValue("QT_QPA_FB_DISABLE_INPUT")) createInputHandlers(); }
QLinuxFbScreen::initialize()打开fb设备,从中获取屏幕参数,映射frame buffer内存,并以此创建QImage对象,为后续doRedraw()和grabWindow()做好准备。
bool QLinuxFbScreen::initialize() { QRegularExpression ttyRx(QLatin1String("tty=(.*)")); QRegularExpression fbRx(QLatin1String("fb=(.*)")); QRegularExpression mmSizeRx(QLatin1String("mmsize=(\\d+)x(\\d+)")); QRegularExpression sizeRx(QLatin1String("size=(\\d+)x(\\d+)")); QRegularExpression offsetRx(QLatin1String("offset=(\\d+)x(\\d+)")); QString fbDevice, ttyDevice; QSize userMmSize; QRect userGeometry; bool doSwitchToGraphicsMode = true; // Parse arguments for (const QString &arg : qAsConst(mArgs)) { QRegularExpressionMatch match; if (arg == QLatin1String("nographicsmodeswitch")) doSwitchToGraphicsMode = false; else if (arg.contains(mmSizeRx, &match)) userMmSize = QSize(match.captured(1).toInt(), match.captured(2).toInt()); else if (arg.contains(sizeRx, &match)) userGeometry.setSize(QSize(match.captured(1).toInt(), match.captured(2).toInt())); else if (arg.contains(offsetRx, &match)) userGeometry.setTopLeft(QPoint(match.captured(1).toInt(), match.captured(2).toInt())); else if (arg.contains(ttyRx, &match)) ttyDevice = match.captured(1); else if (arg.contains(fbRx, &match)) fbDevice = match.captured(1); } if (fbDevice.isEmpty()) {--如果没有指定fb设备,下面选择默认设备。 fbDevice = QLatin1String("/dev/fb0"); if (!QFile::exists(fbDevice)) fbDevice = QLatin1String("/dev/graphics/fb0"); if (!QFile::exists(fbDevice)) { qWarning("Unable to figure out framebuffer device. Specify it manually."); return false; } } // Open the device mFbFd = openFramebufferDevice(fbDevice);--打开fb设备,对应内核fb_open()函数。。 if (mFbFd == -1) { qErrnoWarning(errno, "Failed to open framebuffer %s", qPrintable(fbDevice)); return false; } // Read the fixed and variable screen information fb_fix_screeninfo finfo; fb_var_screeninfo vinfo; memset(&vinfo, 0, sizeof(vinfo)); memset(&finfo, 0, sizeof(finfo)); if (ioctl(mFbFd, FBIOGET_FSCREENINFO, &finfo) != 0) {--获取当前fb设备的固定信息,对应内核fb_ioctl()函数的命令FBIOGET_FSCREENINFO。 qErrnoWarning(errno, "Error reading fixed information"); return false; } if (ioctl(mFbFd, FBIOGET_VSCREENINFO, &vinfo)) {--获取当前fb设备的可变信息,包括分辨率、像素位宽等等,对应内核do_fb_ioctl()函数的命令FBIOGET_VSCREENINFO。 qErrnoWarning(errno, "Error reading variable information"); return false; } mDepth = determineDepth(vinfo);--得出当前fb设备的色深。 mBytesPerLine = finfo.line_length;--获取一行数据长度。 QRect geometry = determineGeometry(vinfo, userGeometry); mGeometry = QRect(QPoint(0, 0), geometry.size()); mFormat = determineFormat(vinfo, mDepth);--获取每个像素的格式,在QImage::Format定义。 mPhysicalSize = determinePhysicalSize(vinfo, userMmSize, geometry.size()); // mmap the framebuffer mMmap.size = finfo.smem_len;--frambuffer的大小。 uchar *data = (unsigned char *)mmap(0, mMmap.size, PROT_READ | PROT_WRITE, MAP_SHARED, mFbFd, 0);--将内核中framebuffer映射为可读写,大小为finfo.smem_len大小的一块buffer。对应内核fb_mmap()函数。 if ((long)data == -1) { qErrnoWarning(errno, "Failed to mmap framebuffer"); return false; } mMmap.offset = geometry.y() * mBytesPerLine + geometry.x() * mDepth / 8;--预留一帧+一行大小的数据。 mMmap.data = data + mMmap.offset;--QImage的buffer从mMmap.offset开始。 QFbScreen::initializeCompositor(); mFbScreenImage = QImage(mMmap.data, geometry.width(), geometry.height(), mBytesPerLine, mFormat);--QImage类是设备无关的图像,可以进行像素及操作,也可被用作绘图设备。 mCursor = new QFbCursor(this); mTtyFd = openTtyDevice(ttyDevice); if (mTtyFd == -1) qErrnoWarning(errno, "Failed to open tty"); switchToGraphicsMode(mTtyFd, doSwitchToGraphicsMode, &mOldTtyMode); blankScreen(mFbFd, false);--对应内核函数fb_ioctl()的命令FBIOBLANK。 return true; }
2.1.2 事件触发屏幕内容更新
当QFbScreen::event()判断有时间发生需要更新屏幕时,调用doRedraw刷新屏幕:
bool QFbScreen::event(QEvent *event) { if (event->type() == QEvent::UpdateRequest) {--事件需要更新屏幕。 doRedraw(); mUpdatePending = false; return true; } return QObject::event(event); }
2.1.3 关闭Framebuffer设备
在QLinuxFbScreen对象析构时,关闭Framebuffer设备:
QLinuxFbScreen::~QLinuxFbScreen() { if (mFbFd != -1) { if (mMmap.data) munmap(mMmap.data - mMmap.offset, mMmap.size); close(mFbFd);--对应内核fb_release()函数。 } if (mTtyFd != -1) resetTty(mTtyFd, mOldTtyMode); delete mBlitter; }
2.2 Input设备
QLinuxFbIntegration对象创建了一系列Input处理函数:触摸屏、键盘、鼠标等。
void QLinuxFbIntegration::createInputHandlers()
{
#if QT_CONFIG(libinput)
if (!qEnvironmentVariableIntValue("QT_QPA_FB_NO_LIBINPUT")) {
new QLibInputHandler(QLatin1String("libinput"), QString());
return;
}
#endif
#if QT_CONFIG(tslib)
bool useTslib = qEnvironmentVariableIntValue("QT_QPA_FB_TSLIB");
if (useTslib)
{
new QTsLibMouseHandler(QLatin1String("TsLib"), QString());--如果QT_QPA_FB_TSLIB置1,则创建QTsLibMouseHandler对象。
}
#endif
#if QT_CONFIG(evdev)
m_kbdMgr = new QEvdevKeyboardManager(QLatin1String("EvdevKeyboard"), QString(), this);--创建基于evdev的keyboard对象。
new QEvdevMouseManager(QLatin1String("EvdevMouse"), QString(), this);--创建基于evdev的Mouse对象。
#if QT_CONFIG(tslib)
if (!useTslib)
#endif
new QEvdevTouchManager(QLatin1String("EvdevTouch"), QString() /* spec */, this);--和tslib冲突。
#endif
}
2.2.1 基于tslib的Mouse
QTsLibMouseHandler构造函数中调用libts.so的ts_setup()打开触摸屏并进行配置:
QTsLibMouseHandler::QTsLibMouseHandler(const QString &key, const QString &specification, QObject *parent) : QObject(parent), m_rawMode(!key.compare(QLatin1String("TslibRaw"), Qt::CaseInsensitive)) { qDebug(qLcTsLib) << "Initializing tslib plugin" << key << specification; setObjectName(QLatin1String("TSLib Mouse Handler")); m_dev = ts_setup(nullptr, 1);--打开触摸屏设备,返回句柄m_dev。 if (!m_dev) { qErrnoWarning(errno, "ts_setup() failed"); return; } #ifdef TSLIB_VERSION_EVENTPATH /* also introduced in 1.15 */ qCDebug(qLcTsLib) << "tslib device is" << ts_get_eventpath(m_dev); #endif m_notify = new QSocketNotifier(ts_fd(m_dev), QSocketNotifier::Read, this);--使用ts_fd(m_dev)和QSocketNotifier构造一个实例,表示先关设备节点的可读状态。 connect(m_notify, &QSocketNotifier::activated, this, &QTsLibMouseHandler::readMouseData);--将activated信号连接到readMouseData函数,当触摸屏设备节点可读时,调用QTsLibMouseHandler::readMouseData()获取数据。 }
QTsLibMouseHandler::readMouseData()中读取触摸屏设备数据,并调用QWindowSystemInterface::handleMouseEvent()进行处理。
static bool get_sample(struct tsdev *dev, struct ts_sample *sample, bool rawMode) { if (rawMode) { return (ts_read_raw(dev, sample, 1) == 1); } else { return (ts_read(dev, sample, 1) == 1);--读取单点触摸数据。 } } void QTsLibMouseHandler::readMouseData() { ts_sample sample; while (get_sample(m_dev, &sample, m_rawMode)) {--读取数据。 bool pressed = sample.pressure; int x = sample.x; int y = sample.y; // coordinates on release events can contain arbitrary values, just ignore them if (sample.pressure == 0) { x = m_x; y = m_y; } if (!m_rawMode) { //filtering: ignore movements of 2 pixels or less int dx = x - m_x; int dy = y - m_y; if (dx*dx <= 4 && dy*dy <= 4 && pressed == m_pressed) continue; } QPoint pos(x, y);--将读取到的x、y数据转换成QPoint。 QWindowSystemInterface::handleMouseEvent(nullptr, pos, pos, pressed ? Qt::LeftButton : Qt::NoButton, Qt::NoButton, QEvent::None); m_x = x; m_y = y; m_pressed = pressed; } }
析构函数中关闭触摸屏设备。
QTsLibMouseHandler::~QTsLibMouseHandler() { if (m_dev) ts_close(m_dev);--关闭触摸屏设备。 }
2.2.2 基于evdev的Keyboard
2.2.3 基于evdev的Mouse
3. Linux DRM和framebuffer驱动
make linux-menuconfig中进入Device Driver->Graphics support,打开Direct Rendering Manager和DRM Support for PL111 CLCD driver:
pl111驱动注册framebuffer设备:
pl111_amba_driver ->pl111_amba_probe ->pl111_versatile_init ->pl111_vexpress_clcd_init ->drm_fbdev_generic_setup ->drm_client_init ->drb_fbdev_client_hotplug ->drm_fb_helper_initial_config ->__drm_fb_helper_initial_config_and_unlock ->register_framebuffer
dtb配置:
clcd@10020000 { compatible = "arm,pl111", "arm,primecell"; reg = <0x10020000 0x1000>; interrupt-names = "combined"; interrupts = <0 44 4>; clocks = <&oscclk1>, <&oscclk2>; clock-names = "clcdclk", "apb_pclk"; /* 1024x768 16bpp @65MHz */ max-memory-bandwidth = <95000000>; port { clcd_pads_ct: endpoint { remote-endpoint = <&dvi_bridge_in_ct>; arm,pl11x,tft-r0g0b0-pads = <0 8 16>; }; }; };
4 QT测试程序
在/etc/profile中增加一下环境变量:
export TSLIB_TSDEVICE=/dev/input/event0
export QT_QPA_FB_TSLIB=1
export QT_QPA_PLATFORM=linuxfb
export QT_QPA_FONTDIR=/usr/share/fonts
启动程序进行动画和触摸屏测试:
/usr/lib/qt/examples/widgets/animation/animatedtiles/animatedtiles
结果如下:
Linux DRM子系统属于GPU一部分,更多参考《DRM子系统》。关于Framebuffer,更多参考《Frame Buffer Library — The Linux Kernel documentation》《Mine of Information - Linux Framebuffer Drivers (vonos.net)》《Linux Framebuffer 实验》。
《Linux Graphics Drivers: an Introduction (people.freedesktop.org)》中介绍了Linux显示相关驱动,包括Framebuffer、DRM、OpenGL等。
参考文档: