LXR | KVM | PM | Time | Interrupt | Systems Performance | Bootup Optimization

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 &paramList)
    : 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()的命令FBIOBLANKreturn 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等。

 参考文档:

Qt for Embedded Linux | Qt 5.15

posted on 2023-05-27 23:59  ArnoldLu  阅读(7685)  评论(0编辑  收藏  举报

导航