65.9K
CodeProject 正在变化。 阅读更多。
Home

树莓派裸机开发的挫折

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (17投票s)

2016年11月21日

CPOL

10分钟阅读

viewsIcon

41425

downloadIcon

301

与树莓派度过两周的失败和思考

 

第二部分现已推出: https://codeproject.org.cn/Articles/1158245/More-Fun-frustration-and-laughs-with-the-Pi-Part-I

引言

我在抽屉里放了一块树莓派大约 6 个月,一直想玩玩。临近圣诞节,工作节奏慢了下来,我想我会有时间玩玩这个新玩具。从我的嵌入式背景来看,树莓派简直是我的天堂,它拥有比我通常接触到的内存和速度都多得多。

背景

现在的问题是我会在上面编程什么。嗯,在我工作的领域里,一个新奇的东西就是图形屏幕,所以无论我做什么,肯定都跟屏幕有关。回想我早期的编程生涯,我曾通过将 Borland 最初发布的 TURBO VISION 在 C++ 和 Pascal 中完整地移植图形化而引起了公司的注意。大部分代码至今仍存在于 FreePascal & FreeVision 库中。

作为我的第一个大型商业项目,我开始在一个名为 Darkside 的通用图形框架上进行 Windows 的无文档移植。我们已经运行了基本版本,没有遇到什么大问题,但情况很快发生了变化。微软重新发布了 Windows CE,这次定价合理,并且 Linux 上也发布了 Wine。对于我们的商业项目来说,为了一个小小的许可费用而重新发明轮子是很愚蠢的,所以我们欣然转向了 Windows CE,并将 Darkside 代码归档。

所以我想,这也许是个好机会,可以掸掉旧代码上的灰尘,尝试将 Darkside 移植到树莓派上,作为一个小型操作系统,一种微型 Windows。

Using the Code

决定了任务,我掸掉了旧代码上的灰尘。本质上,Darkside 就像 Turbo Vision (Free Vision) 一样,是一个事件驱动的系统,你可以在它们之下耦合多任务处理,但这并非严格要求。所以为了最初保持简单,我会先让事件驱动工作,然后在稍后阶段考虑在它之下实现 pthreads。

Windows 的常规消息循环建立了一个理想的事件驱动系统,这并不奇怪,它继承自 Windows 3.1,那是一个事件驱动系统,融合了协作式多任务内核。

常规的 Windows 消息循环形式如下:

    // Standard message handler loop BY THE MSDN BOOK
    // https://msdn.microsoft.com/en-us/library/ms644936(v=vs.85).aspx
    MSG msg = { 0 };
    BOOL bRet;
    while ((bRet = GetMessage(&msg, 0, 0, 0)) != 0) {
        if (bRet == -1) {
            // handle the error and possibly exit
        } else {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }

所以 GetMessage 获取事件,DispatchMessage 分发事件。

为了使 Darkside 具有可移植性,硬件需要实现三个外部函数调用。这三个函数涵盖了鼠标、键盘和定时器这三个主要的输入事件。本质上,这三个函数中的代码对每个硬件设置都是唯一的,你可以创建自己的例程来处理它们,并通过 Darkside API 中的一个特殊函数将处理程序设置为你的代码。

所以我决定先做最简单的,也就是设置定时器。在我 Darkside 的实现中,你需要提供一个函数,该函数提供一个 1 毫秒的滴答计数,与 Windows 中的 GetTickCount API 函数相匹配。在调用 GetMessage 的底部有一个 NextTimerMsg 函数,它会获取下一个已过超时计数的定时器,并将其作为 WM_TIMER 消息。这是一个非常简单的函数,它从定时器设置数组的第一个条目开始,检查该定时器上设置的间隔时间是否已过。如果已过,它会准备一个匹配的 WM_TIMER 消息,并进行设置,以便下次调用 NextTimerMsg 时,它从下一个定时器开始。出于显而易见的原因,你不想总是从数组的第一个条目开始搜索定时器超时,因为你希望所有定时器都能得到平等的对待。我的实现看起来是这样的

/*-INTERNAL: NextTimerMsg---------------------------------------------------
 If there is a timer has exceeded its timeout then return timer message.
 Internal message routine so msg pointer guaranteed to be valid
 12Nov16 LdB
 --------------------------------------------------------------------------*/
static void NextTimerMsg (LPMSG msg) {
    if ((TimerCount > 0) && (TimerTickFunc)) {     // Basic check that timers are set and 
                                                   // system is running
        unsigned short i;
        i = TimerChkStart;                         // We will resume at next timer check position
        do {
            if (TimerQueue[i].TimerId > 0) {       // Check timer is in use
                DWORD tick, elapsed;
                tick = TimerTickFunc();            // Get timer tick
                if (tick < TimerQueue[i].LastTime) {  // Timer rolled (** Remember it rolls min 
                                                      // every 49 days **)
                    elapsed = ULONG_MAX - TimerQueue[i].LastTime;  // Amount of ticks left until it 
                                                                   // would have rolled
                    elapsed += (tick + 1);         // Now add the current tick to the amount from above
                } else {                           // Time was in correct order
                    elapsed = tick - TimerQueue[i].LastTime;       // Simply subtract the two times
                }
                if (elapsed > TimerQueue[i].Interval) {        // Elapsed time exceeds timer interval
                    TimerQueue[i].LastTime += TimerQueue[i].Interval; // Add the timer interval .. 
                                                                      // we want pulses near interval.
                    msg->hwnd = TimerQueue[i].TimerWindow;            // Set the message window handle
                    if (msg->hwnd == 0) msg->hwnd = (HWND)FocusedWnd; // If window still zero try the 
                                                                      // focused window
                    if (msg->hwnd == 0) msg->hwnd = (HWND)AppWindow;  // If window handle still valid 
                                                                      // try app window
                    msg->message = WM_TIMER;                          // WM_TIMER message
                    msg->wParam = (WPARAM)TimerQueue[i].TimerId;      // Set timer id to wParam
                    msg->lParam = (LPARAM)TimerQueue[i].UserTimerFunc;// Set user timer function 
                                                                      // to lParam
                }
            }
            i++;                                  // Increment to next timer to check
            if (i >= TimerMax) i = 0;             // Roll back to first timer if it exceeds array
        } while ((msg->message == WM_NULL) && (i != TimerChkStart));// Exit if we get a timer message 
                                                            // or we have check each timer
        TimerChkStart = i;                                  // Hold the timer we start next check at
    }
}

现在我们的 Windows 版本 DispatchMessage API 函数本质上会看到一个 WM_TIMER 消息,如果 Timer 函数指针已设置,它会直接调用它,否则会将 WM_TIMER 消息发送到选定的窗口。在我们的实验代码中,后者是不适用的,因为我们还没有设置窗口。无论如何,DispatchMessage 的形式如下:

/*-DispatchMessage----------------------------------------------------------
 The DispatchMessage function dispatches a message to a window procedure.
 It is typically used to dispatch a message retrieved by GetMessage function.
 11Nov16 LdB
 --------------------------------------------------------------------------*/
BOOL DispatchMessage (LPMSG lpMsg){
    PWNDSTRUCT Wp;
    if ((lpMsg) && (lpMsg->message != WM_NULL)) {                // Check ptr and message valid
        Wp = (PWNDSTRUCT)lpMsg->hwnd;                            // Typecast window handle to ptr
        if (Wp == NULL) Wp = AppWindow;                          // Zero means application window
        if ((lpMsg->message == WM_TIMER) && (lpMsg->lParam)){    // Timer messages with 
                                                             // valid function pointer called directly
            TIMER* timer = TimerFromID((UINT_PTR)lpMsg->wParam); // Find the timer record
            timer->UserTimerFunc(lpMsg->hwnd, WM_TIMER,
                timer->TimerId, timer->LastTime);                // Call timer function pointer
            return (1);                                          // Return successful dispatch
        }
        if (Wp) return ((INT) Wp->lpfnWndProc(lpMsg->hwnd,
            lpMsg->message,    lpMsg->wParam, lpMsg->lParam));   // Call the handle if Wp valid
    }
    return (0);                                                  // Either message or handler invalid
}

一切看起来都很好,所以我认为我最好检查一下所有东西是否都正常工作,为此,我会设置一个 Windows 控制台程序(但不要包含 Windows.h)。相反,我们包含我们的 "Darkside.h",并制作了一个简单的 5 行 C 文件,它将实际的 Windows GetTickCount 链接进来。所以,它被称为 user.h,并且非常直接。

//User.H
#ifndef _USER_
#define _USER_
DWORD GetTimerTick (void);
#endif

//User.C
#include <windows.h>
#include "user.h"
DWORD GetTimerTick(void) {
    return (GetTickCount());
}

你明白了,我可以导入 Windows 的 GetTickCount 而无需引入整个 Windows.h

现在是我的测试代码

#include <stdio.h>            // Needed for printf
#include "Darkside.h"        // Our windows replacement
#include "User.h"            // Gives use GetTimerCount function

// Some count variables
int count1 = 0;
int count2 = 0;

// Forward declare our functions
void CALLBACK MyTimerFunc1(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime);
void CALLBACK MyTimerFunc2(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime);

int main(void) {

    printf("Press escape to exit\r\n\r\n");
    printf("TIMER 1 = %i\r\n", count1);
    printf("TIMER 2 = %i\r\n", count2);

    /* OKAY COUPLE THE TIMER TICK FUNCTION TO DARKSIDE */
    SetGetTimerTickHandler(GetTimerTick);

    // Start some timers like you do in windows
    SetTimer(0, 0, 1000, MyTimerFunc1);  // 1 second
    SetTimer(0, 0, 2500, MyTimerFunc2);  // 2.5 sec
  
    // Standard message handler loop BY THE MSDN BOOK
    // https://msdn.microsoft.com/en-us/library/ms644936(v=vs.85).aspx
    MSG msg = { 0 };
    BOOL bRet;
    while ((bRet = GetMessage(&msg, 0, 0, 0)) != 0) {
        if (bRet == -1) {
            // handle the error and possibly exit
        } else {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }

   return(0);
}

/* FUNCTION TIMER 1 */
void CALLBACK MyTimerFunc1(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime) {
    count1++;
    printf("TIMER 1 = %i\r\n", count1);
}

/* FUNCTION TIMER 2 */
void CALLBACK  MyTimerFunc2(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime) {
    count2++;
    printf("TIMER 2 = %i\r\n", count2);
}

一切正常,我得到了预期的输出。

一切就绪后,我开始在树莓派上实现。Brian Sidebotham 在 valvers 网站上发表了一些关于树莓派裸机编程的精彩文章(http://www.valvers.com/open-software/raspberry-pi/step01-bare-metal-programming-in-cpt1/)。

我开始做教程 1-3,没有遇到什么真正的问题。他的代码中有两个奇怪的编译器错误,因为我违背了他的建议,使用了 GCC 版本 5.4 而不是他建议的 4.7。错误似乎在优化器中,我还没有弄清楚是他的汇编器还是编译器有问题(我对 Arm 汇编器的经验几乎为零)。无论如何,解决方案很简单:使用 pragma 和/或包装器来阻止优化器,一切就绪。所以这让我了解了系统定时器,这很容易,一个不错的 1Mhz 系统时钟,滚动成一个 64 位定时器结构。所以很容易生成我需要的接口函数。

/*-GetTickCount-------------------------------------------------------------
 We are going to try and match GetTickCount on windows which produces return
 value of the number of milliseconds that have elapsed since the system was
 started. So it frequency is 1000Hz. The raspberry Pi timer is 1Mhz so we need
 to divid it down by 1000. Being 64 bits we are fortunate as we have more bits
 than needed for our 32 bit result we need.
 20Nov16 LdB
 --------------------------------------------------------------------------*/
unsigned long GetTickCount (void) {
    rpi_sys_timer_t    Val = *rpiSystemTimer;                 // Fetch all the timer registers
    unsigned long long resVal = Val.counter_hi;               // Fetch high 32 bits                
    resVal = resVal << 32;                                    // Roll to upper 32 bits
    resVal |= rpiSystemTimer->counter_lo;                     // Add the lower 32 bits in
    resVal /= 1000;                                           // Divid by 1000
    return((unsigned long)resVal);                            // Return the 32 bit result
}

我以为我万事俱备,直到我开始编译代码时,才发现大问题。我的简单程序在写入屏幕的控制台中出现链接器错误,就在那一刻,我才明白 Brian 在启动汇编文件中所做的事情。对我来说,细节丢失的是,标准 GCC 编译器根本无法进行控制台输出……哇,我很久没遇到这种情况了。即使在最基本的 C 嵌入式系统中,控制台也至少会连接到一个串行端口。

我回到 Brian SideBotham 的教程 4 和 5,没有控制台输出的原因变得很明显,大部分图形引擎都隐藏在一个糟糕的二进制块中,文档稀少、不完整,而且大部分都是道听途说。我怀疑编译器程序员像我一样畏缩了。Brian 在文章 5 中有一小部分,他做了一些接线和代码来为串行端口提供控制台输出,这至少对我来说是最低限度的,而且几乎所有最小的 Micro C 编译器都这样做。然而,在他文章的一个补丁中,他已经意识到视频浮点单元未启用的问题。这让我感到一阵寒意,我真的要尝试在一个我没有任何规格、也不知道它在做什么以及涉及的时间是什么的平台上构建像图形操作系统一样图形密集的东西吗?即使在 Brian Sidebottom 的教程 5 中,你也可以看到屏幕撕裂,因为他无法组织更好的屏幕访问。

然后我在论坛上阅读,字体库对许多人来说都存在问题。对我来说,这微不足道,我拥有 C 代码中的位图字体和 TrueType 字体。四边贝塞尔曲线以及如何有效地用字体提示进行扫描线绘制,远远超出了初学者程序员的范围,但出于商业需要,我将其作为标准库。同样,对于许多人来说,图形图元也很成问题,而对我来说,这也很容易。我拥有大量的视频例程库,大多数都允许细粒度区域(如 PI 的帧缓冲区),因为代码可以追溯到 VESA 兼容时代,当时所有 SuperVGA 卡都有精细的窗口。

我的问题不在于以上任何一点,这可能是阻止了其他人。即使要在屏幕上显示鼠标光标,你也需要理想地将其在屏幕上和屏幕外进行遮罩。这意味着需要与屏幕进行双向通信,包括读取和写入屏幕,并且没有可用的时序细节。你通过树莓派上的邮箱通道与屏幕通信,我不知道同步情况如何。如果我发送一个写入消息,然后立即发送一个读取消息,我收到的像素是我原始的像素,还是我刚刚发送消息的那个像素?即使我在我的板子上测试它,我也无法知道所有板子都会有相同的反应,因为我真正需要的是视频单元的规格,而这显然受到严格的保密协议的约束。这种双缓冲在 Windows 操作系统中被广泛使用,每次拖动窗口时,你实际上都希望重绘最少量的屏幕,因此你会组织带有裁剪限制的读/写缓冲区。基本上,我在这个项目中需要对图形进行的所有操作都将需要对视频单元进行读/写和遮罩访问。

然后我查阅了 Linux 的源代码,看看它们是如何处理屏幕的,果然如我所料,一切都隐藏在薄薄的封装例程中,这些例程的注释就像我能描述的那样,外语。

我现在明白为什么网上几乎所有的树莓派裸机的东西都是闪烁灯光或脉冲 IO 端口。要使用比基本功能更高级的图形功能,就意味着要进入 Linux,这样你才能访问由 Arm 作为供应商开发的驱动程序和例程(当然,他们拥有所有详细信息)。

我并不讨厌 Linux。它对于我从事的几乎所有嵌入式项目来说都太大了。我曾在 Snapgear 防火墙代码和几个 FPGA Cortex 核心板上使用过 ucLinux,但这大概是我做过的仅有的足够大的能够容纳它的产品。

所以,PI 用于我的 Darkside 项目已经告吹了,我太不舒服了,不愿意投入大量时间在一个我无法确定视频访问规格是什么的东西上。

关注点

所以,我从树莓派那里学到了什么?首先,如果你想做图形编程,它是一个非常糟糕的裸机目标。坦率地说,我不会浪费时间将一个完整的图形引擎移植到一个你不知道性能如何,并且文档如此糟糕的平台上。

所以我想我的树莓派将要回到抽屉里,直到我需要一个非常小的 Linux 设置时再说。

我相当失望,但我已经把旧代码翻出来了,所以我将寻找另一个目标板,并可能继续下去。就像很多编程一样,最难的部分是开始。

我包含了源代码,这是一个基本的事件驱动消息系统,如果有人想玩玩的话。

更新和新文章即将发布

它来了……第二部分

https://codeproject.org.cn/Articles/1158245/More-Fun-frustration-and-laughs-with-the-Pi-Part-I

对于一个 51K 的 IMG 文件,SD 卡上只有 3 个文件,到目前为止还不错。

© . All rights reserved.