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

树莓派的更多乐趣、挫折与欢笑……第二部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (20投票s)

2016 年 11 月 28 日

CPOL

13分钟阅读

viewsIcon

31876

downloadIcon

1047

树莓派的更多玩耍、哭泣与欢笑

*** 新版本即将推出 USB  可用(见底部)

预编译的 Pi2 二进制文件但未经测试。我使用了这些设置“-mfpu=neon -mfloat-abi=hard -march=armv7-a -mtune=cortex-a7”,我认为这是正确的。很希望能有一位拥有 Pi2 的用户来检查一下 :-)

预编译的 Pi3 二进制文件,但与 Pi2 一样未经测试。我使用了这些设置“-mfpu=neon-vfpv4 -mfloat-abi=hard -march=armv8-a -mtune=cortex-a53”。我在汇编器起始文件第 131 行收到一个警告,我会离线查看。

armc-start.S:131: 此协处理器寄存器访问在 ARMv8 中已弃用

引言

在我上一篇文章中,我以对树莓派的许多挫折结束,但你知道嵌入式程序员的故事,我们 thrives on a challenge(乐于接受挑战)。

所以好消息是,我本周末已经让所有东西以及比我预期的更多东西都正常工作了。这是我当前工作点的截图,我将在文章顶部提供源代码和磁盘映像。

所以,这有一些图形图元:四边形和三次贝塞尔曲线,就像 TrueType 字体使用的一样。基本的窗口和文本,以及一个调试控制台。然后,我将代码传递到我上一篇文章中的 Darkside 代码,定时器开始工作,消息发布,产生了我预期的结果。我将在文章后面详细介绍所有这些。

背景

所有这些都不容易,我最大的教训是:不要相信网上任何关于 C 语言在树莓派上的代码,因为大多数显然不是由专注于嵌入式开发的工程师编写的。如果你不是那种人,这将是一个更大的挑战,因为有很多错误和许多半成品。

所以在这篇文章中,我将介绍

  1.  我在 Windows 上使用的工具链,我会边用边讲,欢迎您使用其他工具链和不同的操作系统,但在那种情况下,您需要自己解决问题。
  2.  在 C 语言下访问硬件寄存器,以及 VOLATILE 和 CONST 关键字的作用
  3.  当前代码的状态:它是什么,它不是什么。
  4.  我近期的计划以及可能的后续文章。
  5. 未来和想法等

工具链

所以,我在 Windows 上编程,我的编辑器是 Visual Studio 2015 Community。这是我所有常规编码使用的,我非常熟悉它,而且它是一流的(除了安装程序,也许是有史以来最糟糕的安装程序)。

代码的实际编译是通过 ARM 发布的 Windows gcc-arm-none-eabi 版本完成的。我使用官方 ARM 网站的最新版本 5.4,链接如下:

https://developer.arm.com/open-source/gnu-toolchain/gnu-rm/downloads

很多人建议使用旧的 4.7 版本,因为他们认为较新版本有 bug。在我尝试的过程中,我实际上发现 4.7 版本实际上 bug 更多。这种愚蠢的说法是基于他们使用代码风格的经验,我们将在下一节中更深入地讨论这个问题。现在,让我们回到工具链。我将下载内容安装到一个编译器工具目录中,您需要记录/记住该目录的路径,我将称之为 $TOOLPATH。

现在,我设置了一个解决方案目录,我将在其中放置所有代码以及构成我正在进行的项目的任何其他内容。我称之为 $SOLUTIONPATH。我不是凭空捏造的,Visual Studio 称它们为这些名称,所以我只是照搬。在我的解决方案目录中,我有一个单独的 MAKE.BAT 文件,我将根据需要进行编辑。当您下载示例代码时,您会看到所有这些,但 $TOOLPATH 对您来说将是错误的。

目前,MAKE.BAT 文件看起来像这样:

@REM COMPILER COMMAND LINE
..\gcc_pi_5_4\bin\arm-none-eabi-gcc -O2 -mfpu=vfp -mfloat-abi=hard -march=armv6zk -mtune=arm1176jzf-s -nostartfiles -g -Wl,-T,rpi.x main.c armc-cstubs.c armc-start.S RPi-Hardware.c Darkside.c User.c -o kernel.elf -lc -lm
@echo off
if %errorlevel% EQU 1 (goto build_fail)

@REM LINKER COMMAND LINE
@echo on
..\gcc_pi_5_4\bin\arm-none-eabi-objcopy kernel.elf -O binary kernel.img
@echo off
if %errorlevel% EQU 1 (goto build_fail) 

echo BUILD COMPLETED NORMALLY
exit /b 0

:build_fail
echo ********** BUILD FAILURE **********
exit /b 1

该文件非常简单,我们调用一次编译器,并带有许多参数。如果编译器返回错误,它将跳转到 build failure(构建失败)。如果成功,它将继续到链接器及其命令行。如果程序出错退出,它也会跳转到 build failure。如果一切正常,您将看到“BUILD COMPLETED NORMALLY”(构建正常完成),否则您将看到“********** BUILD FAILURE **********”(构建失败)。

如果您愿意,可以通过单击目录中的批处理文件来运行它,但我将向您展示如何将其集成到 Visual Studio 中。

目前,我们需要讨论 $TOOLPATH,在我上面的 .bat 文件中是“..\gcc_pi_5_4\bin\arm-none-eabi-gcc”。原因是我的机器上,我将 ARM 工具安装到了 G:\PI\gcc_pi_5_4,而我的解决方案目录是 G:\PI\Darkside,.BAT 文件就位于那里。我使用的路径是从我的解决方案目录(.BAT 文件所在的位置)到编译器文件“arm-none-eabi-gcc.exe”的相对路径。如果您迷失或卡住了,请使用绝对路径,例如,我可以改为使用“g:\pi\gcc_pi_5_4\bin\arm-none-eabi”而不是“.\gcc_pi_5_4\bin\arm-none-eabi-gcc”。无论如何,长话短说,您需要更改编译器和链接器行上的路径。如果您使用的是 PI2 或 PI3,还需要选择正确的处理器。

为了将其集成到 Visual Studio 中,我只是从菜单选项中选择添加外部工具,您可以看到我输入的内容。您可以看到我只是将工具路径和解决方案目录设置为我机器上实际的路径。您还会注意到我勾选了使用控制台窗口,这样批处理文件就会在我的控制台窗口中报告。

正确操作后,您的项目将出现在外部工具列表中。如果您想执行下一步,请记下它在列表中的编号。在我这里是 6……从“创建 GUID”开始计数。

使用以下链接的说明,您可以为您的外部工具在工具栏上添加一个图标(您需要上面提到的编号……对我来说是 6)。

https://blogs.msdn.microsoft.com/david_kidder/2013/09/18/adding-a-button-to-the-debug-toolbar-in-visual-studio/

如果一切正常,当您单击图标时,它将启动并编译,您将在常规的输出窗口中看到这个:

VOLATILE 和 CONST

正如我所讨论的,关于 GCC 编译器工具链的某个版本是否有 bug 的说法很多,它既有 bug 也有没有 bug :-) 这是真正的嵌入式程序员经常遇到的问题。所以,让我们从头开始,首先遵循 ARM 的建议。

http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.faqs/ka3736.html

现在,当嵌入式供应商的工具提供商给出建议时,我强烈建议您遵循标准,除非您有充分的理由或异议。是的,我知道严格的 C 标准说您可以将事物按不同顺序放置,但他们不像供应商那样负责维护工具链。您可以抱怨这是 bug,或者 just use the standard(只是使用标准)。

int volatile * const ptr;
 |     |     |   |    |
 |     |     |   |    +------> ptr is a
 |     |     |   +-----------> constant
 |     |     +---------------> pointer to a
 |     +---------------------> volatile
 +---------------------------> integer

所以,他们想要“type” “volatile” “ptr” “const” “name” 在设置硬件指针时。同样,我知道 C 标准说它们不必按那个顺序,但供应商提出了建议,即 DO IT, STOP ARGUING(照做,别争了)。对我来说,这很简单,因为我总是那样做的,因为我看到网上许多代码中存在以下问题。

MACRO 宏的麻烦与 volatile。很多 Rapberry 代码没有在用于寄存器访问的 MACRO 宏周围正确地加上括号。如果您在没有正确括号的情况下将 volatile 放在前面,宏扩展会将 volatile 应用到错误的对象上。这是检查 MACRO 宏扩展的命令行:

gcc -E -dD

所以,32 位硬件访问的正确扩展应该看起来像:

static uint32_t volatile* const readwritereg = (uint32_t volatile*)(0x.... addresss..);

// The readonly register version is

static uint32_t const volatile* const readonlyreg = (uint32_t const volatile*)(0x....address...);

让我们解释一下这些位以及它们的作用。

STATIC:用于告诉编译器这个指针只在这里使用,如果它不被触及,它就可以被移除,以及任何使用它的代码。所以,如果您有一个大型库,其中部分代码在构建时未被使用,它将将其删除。您只有在 .H 文件接口上公开指针时才不使用它。

uint32_t 在这种情况下是我们的类型,您可能有 int(如 ARM 示例中)或 long 或代表您寄存器的任何类型。

VOLATILE:按照我们的供应商的要求放置在类型之后,并且还能保护我们免受 MACRO 宏扩展的麻烦。在只读中,我们有“const volatile”,它告诉编译器不能写入它,但它是 volatile 的。技术上,您可以颠倒它们的顺序,但逻辑是编译器在优化器之前运行。编译器想要并会看到 const 并忽略 volatile。volatile 是为了让优化器确保它保留对硬件的访问。所以,按照它们将被处理的顺序排列“const volatile”,先编译器后优化,出现问题的可能性较小。

CONST:这个 const 阻止我们写入指针本身。像这样的代码将触发编译器错误:

readwritereg = (uint32_t volatile*)0x4000;

那段代码实际上并没有错,我只是将寄存器指针设置到一个新的地址,只有我知道我的寄存器在哪里,为什么我要改变它的地址(除非我就是想这样做)。通常这意味着您使用了两个库,并且这两个库为不同的寄存器选择了相同的名称,比如 ARM_TIMER。在不同的库中,您会惊讶地发现它使用了完全不同的地址,因为 ARM 上有多个定时器。

这个 const 是保护,与其说是为了您,不如说是为了防止那些能够访问您的寄存器地址的人更改它并崩溃您的代码。所以,除非出现寄存器移动的奇怪情况,否则请加上它。

其余的应该很清楚。在该方案下,有一个问题您无法防止将只写寄存器读取。

对于更高级的用户,有一个更好的编译器方案,它接受匿名联合体,我将留给您自己研究它。

struct wo_uint32 { uint32_t volatile write; };                        // Hardware unsigned 32 bit write only register
struct ro_uint32 { uint32_t const volatile read; };                    // Hardware unsigned 32 bit read only register
struct rw_uint32 { union {
                    uint32_t const volatile read;                   // Hardware unsigned 32 bit read/write register in read mode
                    uint32_t volatile write;                        // Hardware unsigned 32 bit read/write register in write mode
                   };
                 };

typedef struct rw_uint32* hw_uint32_ptr;                            // Hardware pointer unsigned int 32 bit (read/write)
typedef struct ro_uint32* hw_ro_uint32_ptr;                            // Hardware pointer unsigned int 32 bit (read only)
typedef struct wo_uint32* hw_wo_uint32_ptr;                            // Hardware pointer unsigned int 32 bit (write only)

// LETS SHOW A USE 
static hw_uint32_ptr const arm_sys_timer_lo = (hw_uint32_ptr) (RPI_SYSTIMER_BASE + TIMER_LO_OFFSET);
uint32_t lowCount = arm_sys_timer_hi->read;  // read the timer
arm_sys_timer_hi->write = 0x555;             // write to the timer       

人们所说的许多 bug 是编译器优化掉代码段。对于所有编译器处理硬件来说,这是一个常见问题,如果您想了解更详细的论文,这里有一个起点:

http://www.cs.utah.edu/~regehr/papers/emsoft08-preprint.pdf

Valvers 教程中的几个文件有这个问题。我再说一遍,Brian 写的代码从 C 的角度来看是好的,它只是实现了实现或供应商工具的 bug(任您选择词语)。我尊重 Brian 所做的伟大工作,将责任归咎于他是不对的。

在这个部分,我最后的抱怨是:请停止重新定义类型!标准库定义了所有常规的位类型,看看,只需要 1 行就可以包含它。

#include <stdint.h>

是的,O/S API 定义了 LONG、BOOL、UNSIGNED LONG 等等,但它们有充分的理由这样做,因为这些大小可能会改变。BOOL 可以是 8 位、16 位、32 位甚至 64 位,具体取决于 O/S 所在的处理器。当您处理硬件寄存器时,它们在 ARM 上具有固定的位数,并且永远不会改变,也不能改变,所以,看在全能的份上,使用 <std.int>,因为这是 C 标准。

代码状态

树莓派的当前代码状态可能最好被描述为可怕。我基本上抓取了我想要的所有代码片段,并将它们添加到 RPi_Hardware_H + RPi_Hardware.C 的一个文件组中。这是基于发现当前形式的代码库基本上都存在上述问题。我正在慢慢地逐个文件地修复所有问题。

我实现的第一个修复是,希望正确地让代码在您选择的处理器基础上在各种 RPi 型号之间切换。我只有 Pi 1,所以如果有人能告诉我这是否正确地翻译到了 Pi2 和 Pi3,那就太好了。

#ifdef __ARMEL__                                // Compiling for ARM ... likely Rasberry PI
#if (__ARM_ARCH == 6)                            // Architecture is ARM 6 so its a Original Pi A or B+
#define RPI                                        // Define Rasberry Pi
#endif
#if (__ARM_ARCH == 7)                            // Architecture is ARM 7 so its a Pi 2
#define RPI2                                    // Define Rasberry Pi 2
#endif
#if (__ARM_ARCH == 8)                            // Architecture is ARM 8 so its a Pi 3
#define RPI3                                    // Define Rasberry Pi 3
#endif
#endif

所有地址的更改都取决于这些实现,所以只要您选择了正确的处理器,我相信您就能获得正确的代码地址。

我的实验代码

请将代码视为预发布版本,目前我不会提供太多细节,因为它将在我正式发布时进行更改。目前的代码是为那些想比我更早开始的人准备的。

控制台是一个非常基本的实现终端,但所有控制台 IO 都应该被发送到它。您有责任通过 PiConsole.Redraw()(如果需要)将其保持在前台。文本在控制台内缓冲。我设置的大小是我的选择,这样我就有屏幕的一定区域可以玩。

#define ROWS    10
#define COLUMNS 80

我使用欧拉数学实现了一个完整的整数版 QUAD 和 CUBIC BEZIER。例程中完全没有使用浮点数,但它们会产生正常的舍入误差。嵌入式处理器上的常用技巧是减小循环增量,这样您就可以得到更小的片段,使效果不那么明显。当前的增量步分辨率是 1:1。我发现视频单元似乎有一个 OpenGL 硬件渲染器,如果是这样,我打算在其中实现抗锯齿。

truetype 字母 B 的字形包含在 test.inc 中供我测试。扫描线填充例程首先使用浮点/双精度,然后反向用于我查看问题。它不是 intended to be used(打算被使用),仅仅是为了我检查 TrueType 工作。如果您从代码中找到任何有用的东西,欢迎您随意使用,但我不接受关于它的反馈。

屏幕字体来自包含文件“BitFont.Inc”,您可以在文本编辑器中阅读它。它是一个 4096 字节的数组,包含 256 个字符,每个字符 16 字节,并且是 DOS 代码页 850 的字符位置布局。对于英语来说没问题,但抱歉,外国语言的人会遇到困难。每个字符是 1 个字节(8 位宽),字符高度为 16 字节。您无法更改它,除非包含不同的二进制字体。

我未来的计划

我首先要大声感谢 Brian Sidebotham 的 valvers 网站,他的网站帮助我走到了今天。

我的下一个直接任务是让 USB 正常运行,并让鼠标和键盘上线。USPI 库是免费提供的,但快速看了一下代码让我头痛,原因与上述很多原因相似。只能说 arm GCC 编译器和那段代码可能不兼容,我们就暂不讨论谁对谁错。如果您对 USPI 感兴趣,这是链接:

https://github.com/rsta2/uspi

房间里还剩下两个大象。一是让视频单元获得任何实际性能,二是能够读/写 SD 卡,这两者都涉及 NDA 文档问题。

至少有了控制台,我可以探测和测试事物。然而,键盘 so I could type(以便我能输入)会更有帮助,这就是为什么 USB 被优先处理。

总之,我希望这段代码对您有所帮助,并且不会给您带来太多麻烦。

历史

版本 Alpha 0.1:一个非常糟糕但可以运行的起点。

更新

经过两个周末与 USB 斗争后,新版本即将推出。鼠标和键盘都创建了正确的 WM_xxxxxx 窗口消息。更新 3 正在提交过程中。

© . All rights reserved.