分类 树莓派 下的文章

我们的《树莓派使用入门》的第四篇文章将进入到 Linux 命令行。

在本系列的 第三篇文章 中开始了我们的树莓派探索之旅,我分享了如何安装 Raspbian,它是树莓派的官方 Linux 版本。现在,你已经安装好了 Raspbian 并用它引导你的新树莓派,你已经具备学习 Linux 相关知识的条件了。

在这样简短的文章中去解决像“如何使用 Linux” 这样的宏大主题显然是不切实际的,因此,我只是给你提供一些如何使用树莓派来学习更多的 Linux 知识的一些创意而已。

我们花一些时间从命令行(又称“终端”)开始。自上世纪九十年代中期以来,Linux 的 窗口管理器 和图形界面已经得到长足的发展。如今,你可以在 Linux 上通过鼠标点击来做一些事情了,就如同其它的操作系统一样容易。在我看来,只是“使用 Linux”和成为“一个 Linux 用户”是有区别的,后者至少能够在终端中“遨游“。

如果你想成为一个 Linux 用户,从终端中尝试以下的命令行开始:

  • 使用像 lscdpwd 这样的命令导航到你的 Home 目录。
  • 使用 mkdirrmmvcp 命令创建、删除、和重命名目录。
  • 使用命令行编辑器(如 Vi、Vim、Emacs 或 Nano)去创建一个文本文件。
  • 尝试一些其它命令,比如 chmodchownwcatmorelesstailfreedfpsunamekill
  • 尝试一下 /bin/usr/bin 目录中的其它命令。

学习命令行的最佳方式还是阅读它的 “man 手册”(简称手册);在命令行中输入 man <command> 就可以像上面那样打开它。并且在互联网上搜索 Linux 命令速查表可以让你更清楚地了解命令的用法 —— 你应该会找到一大堆能帮你学习的资料。

Raspbian 就像主流的 Linux 发行版一样有非常多的命令,假以时日,你最终将比其他人会用更多的命令。我使用 Linux 命令行已经超过二十年了,即便这样仍然有一些命令我从来没有使用过,即便是那些我使用的过程中就一直存在的命令。

最后,你可以使用图形环境去更快地工作,但是只有深入到 Linux 命令行,你才能够获得操作系统真正的强大功能和知识。


via: https://opensource.com/article/19/3/learn-linux-raspberry-pi

作者:Andersn Silva 选题:lujun9972 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在本系列文章的第三篇中,我们将教你开始使用树莓派,学习如何安装一个 Linux 操作系统。

如果你按顺序看我们本系列的文章,那么你已经 选择购买 了你的树莓派和外围设备,现在,你将要去使用它。在第三篇文章中,我们来看一下你需要做些什么才能让它启动起来。

与你的笔记本、台式机、智能手机、或平板电脑不一样的是,树莓派上并没有内置存储。而是需要使用一个 Micro SD 卡去存储操作系统和文件。这么做的最大好处就是携带你的文件比较方便(甚至都不用带着树莓派)。不利之处是存储卡丢失和损坏的风险可能很高,这将导致你的文件丢失。因此,只要保护好你的 Micro SD 卡就没什么问题了。

你应该也知道,SD 卡的读写速度比起机械硬件或固态硬盘要慢很多,因此,你的树莓派的启动、读取、和写入的速度将不如其它设备。

如何安装 Raspbian

你拿到新树莓派的第一件事情就是将它的操作系统安装到一个 Micro SD 卡上。尽管树莓派上可用的操作系统很多(基于 Linux 的或非基于 Linux 的都有),但本系列课程将专注于 Raspbian,它是树莓派的官方 Linux 版本。

安装 Raspbian 的最简单的方式是使用 NOOBS,它是 “New Out Of Box Software” 的缩写。树莓派官方提供了非常详细的 NOOBS 文档,因此,我就不在这里重复这些安装指令了。

NOOBS 可以让你选择安装以下的这些操作系统:

再强调一次,我们在本系列的课程中使用的是 Raspbian,因此,拿起你的 Micro SD 卡,然后按照 NOOBS 文档去安装 Raspbian 吧。在本系列的第四篇文章中,我们将带你去看看,如何使用 Linux,包括你需要掌握的一些主要的命令。


via: https://opensource.com/article/19/3/how-boot-new-raspberry-pi

作者:Anderson Silva 选题:lujun9972 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在我们的《树莓派使用入门》系列文章的第二篇中,我们将介绍获取树莓派的最佳途径。

在本系列指南的第一篇文章中,我们提供了一个关于 你应该购买哪个版本的树莓派 的一些建议。哪个版本才是你想要的,你应该有了主意了,现在,我们来看一下如何获得它。

最显而易见的方式 —— 并且也或许是最安全最简单的方式 —— 非 树莓派的官方网站 莫属了。如果你从官网主页上点击 “Buy a Raspberry Pi”,它将跳转到官方的 在线商店,在那里,它可以给你提供你的国家所在地的授权销售商。如果你的国家没有在清单中,还有一个“其它”选项,它可以提供国际订购。

第二,查看亚马逊或在你的国家里允许销售新的或二手商品的其它主流技术类零售商。鉴于树莓派比较便宜并且尺寸很小,一些小商家基于转售目的的进出口它,应该是非常容易的。在你下订单时,一定要关注对卖家的评价。

第三,打听你的极客朋友!你可能从没想过一些人的树莓派正在“吃灰”。我已经给家人送了至少三个树莓派,当然它们并不是计划要送的礼物,只是因为他们对这个“迷你计算机”感到很好奇而已。我身边有好多个,因此我让他们拿走一个!

不要忘了外设

最后一个建设是:不要忘了外设,你将需要一些外设去配置和操作你的树莓派。至少你会用到键盘、一个 HDMI 线缆去连接显示器、一个 Micro SD 卡去安装操作系统,一个电源线、以及一个好用的鼠标。

如果你没有准备好这些东西,试着从朋友那儿借用,或与树莓派一起购买。你可以从授权的树莓派销售商那儿考虑订购一个起步套装 —— 它可以让你避免查找的麻烦而一次性搞定。

现在,你有了树莓派,在本系列的下一篇文章中,我们将安装树莓派的操作系统并开始使用它。


via: https://opensource.com/article/19/3/how-buy-raspberry-pi

作者:Anderson Silva 选题:lujun9972 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在我们的《树莓派使用入门》系列的第一篇文章中,我们将学习选择符合你要求的树莓派型号的三个标准。

本文是《14 天学会树莓派使用》系列文章的第一篇。虽然本系列文章主要面向没有使用过树莓派或 Linux 或没有编程经验的人群,但是肯定有些东西还是需要有经验的读者的,我希望这些读者能够留下他们有益的评论、提示和补充。如果每个人都能贡献,这将会让本系列文章对初学者、其它有经验的读者、甚至是我更受益!

言归正传,如果你想拥有一个树莓派,但不知道应该买哪个型号。或许你希望为你的教学活动或你的孩子买一个,但面对这么多的选择,却不知道应该买哪个才是正确的决定。

关于选择一个新的树莓派,我有三个主要的标准:

  • 成本: 不能只考虑树莓派板的成本,还需要考虑到你使用它时外围附件的成本。在美国,树莓派的成本区间是从 5 美元(树莓派 Zero)到 35 美元(树莓派 3 B 和 3 B+)。但是,如果你选择 Zero,那么你或许还需要一个 USB hub 去连接你的鼠标、键盘、无线网卡、以及某种显示适配器。不论你想使用树莓派做什么,除非你已经有了(假如不是全部)大部分的外设,那么你一定要把这些外设考虑到预算之中。此外,在一些国家,对于许多学生和老师,树莓派(即便没有任何外设)的购置成本也或许是一个不少的成本负担。
  • 可获得性: 根据你所在地去查找你想要的树莓派,因为在一些国家得到某些版本的树莓派可能很容易(或很困难)。在新型号刚发布后,可获得性可能是个很大的问题,在你的市场上获得最新版本的树莓派可能需要几天或几周的时间。
  • 用途: 所在地和成本可能并不会影响每个人,但每个购买者必须要考虑的是买树莓派做什么。因内存、CPU 核心、CPU 速度、物理尺寸、网络连接、外设扩展等不同衍生出八个不同的型号。比如,如果你需要一个拥有更大的“马力”时健壮性更好的解决方案,那么你或许应该选择树莓派 3 B+,它有更大的内存、最快的 CPU、以及更多的核心数。如果你的解决方案并不需要网络连接,并不用于 CPU 密集型的工作,并且需要将它隐藏在一个非常小的空间中,那么一个树莓派 Zero 将是你的最佳选择。

维基百科的树莓派规格表 是比较八种树莓派型号的好办法。

现在,你已经知道了如何找到适合你的树莓派了,下一篇文章中,我将介绍如何购买它。


via: https://opensource.com/article/19/3/which-raspberry-pi-choose

作者:Anderson Silva 选题:lujun9972 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

屏幕04 课程基于屏幕03 课程来构建,它教你如何操作文本。假设你已经有了课程 8:屏幕03 的操作系统代码,我们将以它为基础。

1、操作字符串

能够绘制文本是极好的,但不幸的是,现在你只能绘制预先准备好的字符串。如果能够像命令行那样显示任何东西才是完美的,而理想情况下应该是,我们能够显示任何我们期望的东西。一如既往地,如果我们付出努力而写出一个非常好的函数,它能够操作我们所希望的所有字符串,而作为回报,这将使我们以后写代码更容易。曾经如此复杂的函数,在 C 语言编程中只不过是一个 sprintf 而已。这个函数基于给定的另一个字符串和作为描述的额外的一个参数而生成一个字符串。我们对这个函数感兴趣的地方是,这个函数是个变长函数。这意味着它可以带可变数量的参数。参数的数量取决于具体的格式字符串,因此它的参数的数量不能预先确定。

变长函数在汇编代码中看起来似乎不好理解,然而 ,它却是非常有用和很强大的概念。

这个完整的函数有许多选项,而我们在这里只列出了几个。在本教程中将要实现的选项我做了高亮处理,当然,你可以尝试去实现更多的选项。

函数通过读取格式化字符串来工作,然后使用下表的意思去解释它。一旦一个参数已经使用了,就不会再次考虑它了。函数的返回值是写入的字符数。如果方法失败,将返回一个负数。

表 1.1 sprintf 格式化规则

选项含义
除了 % 之外的任何支付复制字符到输出。
%%写一个 % 字符到输出。
%c将下一个参数写成字符格式。
%d%i将下一个参数写成十进制的有符号整数。
%e将下一个参数写成科学记数法,使用 eN,意思是 ×10 N
%E将下一个参数写成科学记数法,使用 EN,意思是 ×10 N
%f将下一个参数写成十进制的 IEEE 754 浮点数。
%g%e%f 的指数表示形式相同。
%G%E%f 的指数表示形式相同。
%o将下一个参数写成八进制的无符号整数。
%s下一个参数如果是一个指针,将它写成空终止符字符串。
%u将下一个参数写成十进制无符号整数。
%x将下一个参数写成十六进制无符号整数(使用小写的 a、b、c、d、e 和 f)。
%X将下一个参数写成十六进制的无符号整数(使用大写的 A、B、C、D、E 和 F)。
%p将下一个参数写成指针地址。
%n什么也不输出。而是复制到目前为止被下一个参数在本地处理的字符个数。

除此之外,对序列还有许多额外的处理,比如指定最小长度,符号等等。更多信息可以在 sprintf - C++ 参考 上找到。

下面是调用方法和返回的结果的示例。

表 1.2 sprintf 调用示例

格式化字符串参数结果
"%d"1313
"+%d degrees"12+12 degrees
"+%x degrees"24+1c degrees
"'%c' = 0%o"65, 65‘A’ = 0101
"%d * %d%% = %d"200, 40, 80200 * 40% = 80
"+%d degrees"-5+-5 degrees
"+%u degrees"-5+4294967291 degrees

希望你已经看到了这个函数是多么有用。实现它需要大量的编程工作,但给我们的回报却是一个非常有用的函数,可以用于各种用途。

2、除法

虽然这个函数看起来很强大、也很复杂。但是,处理它的许多情况的最容易的方式可能是,编写一个函数去处理一些非常常见的任务。它是个非常有用的函数,可以为任何底的一个有符号或无符号的数字生成一个字符串。那么,我们如何去实现呢?在继续阅读之前,尝试快速地设计一个算法。

除法是非常慢的,也是非常复杂的基础数学运算。它在 ARM 汇编代码中不能直接实现,因为如果直接实现的话,它得出答案需要花费很长的时间,因此它不是个“简单的”运算。

最简单的方法或许就是我在 课程 1:OK01 中提到的“除法余数法”。它的思路如下:

  1. 用当前值除以你使用的底。
  2. 保存余数。
  3. 如果得到的新值不为 0,转到第 1 步。
  4. 将余数反序连起来就是答案。

例如:

表 2.1 以 2 为底的例子

转换

新值余数
137681
68340
34170
1781
840
420
210
101

因此答案是 10001001 2

这个过程的不幸之外在于使用了除法。所以,我们必须首先要考虑二进制中的除法。

我们复习一下长除法

假如我们想把 4135 除以 17。

   0243 r 4
17)4135
   0        0 × 17 = 0000
   4135     4135 - 0 = 4135
   34       200 × 17 = 3400
   735      4135 - 3400 = 735
   68       40 × 17 = 680
   55       735 - 680 = 55
   51       3 × 17 = 51
   4        55 - 51 = 4

答案:243 余 4

首先我们来看被除数的最高位。我们看到它是小于或等于除数的最小倍数,因此它是 0。我们在结果中写一个 0。

接下来我们看被除数倒数第二位和所有的高位。我们看到小于或等于那个数的除数的最小倍数是 34。我们在结果中写一个 2,和减去 3400。

接下来我们看被除数的第三位和所有高位。我们看到小于或等于那个数的除数的最小倍数是 68。我们在结果中写一个 4,和减去 680。

最后,我们看一下所有的余位。我们看到小于余数的除数的最小倍数是 51。我们在结果中写一个 3,减去 51。减法的结果就是我们的余数。

在汇编代码中做除法,我们将实现二进制的长除法。我们之所以实现它是因为,数字都是以二进制方式保存的,这让我们很容易地访问所有重要位的移位操作,并且因为在二进制中做除法比在其它高进制中做除法都要简单,因为它的数更少。

        1011 r 1
1010)1101111
     1010
      11111
      1010
       1011
       1010
          1

这个示例展示了如何做二进制的长除法。简单来说就是,在不超出被除数的情况下,尽可能将除数右移,根据位置输出一个 1,和减去这个数。剩下的就是余数。在这个例子中,我们展示了 1101111 2 ÷ 1010 2 = 1011 2 余数为 1 2。用十进制表示就是,111 ÷ 10 = 11 余 1。

你自己尝试去实现这个长除法。你应该去写一个函数 DivideU32 ,其中 r0 是被除数,而 r1 是除数,在 r0 中返回结果,在 r1 中返回余数。下面,我们将完成一个有效的实现。

function DivideU32(r0 is dividend, r1 is divisor)
  set shift to 31
  set result to 0
  while shift ≥ 0
     if dividend ≥ (divisor << shift) then
       set dividend to dividend - (divisor <&lt shift)
       set result to result + 1
     end if
     set result to result << 1
     set shift to shift - 1
  loop
  return (result, dividend)
end function

这段代码实现了我们的目标,但却不能用于汇编代码。我们出现的问题是,我们的寄存器只能保存 32 位,而 divisor << shift 的结果可能在一个寄存器中装不下(我们称之为溢出)。这确实是个问题。你的解决方案是否有溢出的问题呢?

幸运的是,有一个称为 clz 计数前导零 count leading zeros )的指令,它能计算一个二进制表示的数字的前导零的个数。这样我们就可以在溢出发生之前,可以将寄存器中的值进行相应位数的左移。你可以找出的另一个优化就是,每个循环我们计算 divisor << shift 了两遍。我们可以通过将除数移到开始位置来改进它,然后在每个循环结束的时候将它移下去,这样可以避免将它移到别处。

我们来看一下进一步优化之后的汇编代码。

.globl DivideU32
DivideU32:
result .req r0
remainder .req r1
shift .req r2
current .req r3

clz shift,r1
lsl current,r1,shift
mov remainder,r0
mov result,#0

divideU32Loop$:
  cmp shift,#0
  blt divideU32Return$
  cmp remainder,current
  
  addge result,result,#1
  subge remainder,current
  sub shift,#1
  lsr current,#1
  lsl result,#1
  b divideU32Loop$
divideU32Return$:
.unreq current
mov pc,lr

.unreq result
.unreq remainder
.unreq shift

你可能毫无疑问的认为这是个非常高效的作法。它是很好,但是除法是个代价非常高的操作,并且我们的其中一个愿望就是不要经常做除法,因为如果能以任何方式提升速度就是件非常好的事情。当我们查看有循环的优化代码时,我们总是重点考虑一个问题,这个循环会运行多少次。在本案例中,在输入为 1 的情况下,这个循环最多运行 31 次。在不考虑特殊情况的时候,这很容易改进。例如,当 1 除以 1 时,不需要移位,我们将把除数移到它上面的每个位置。这可以通过简单地在被除数上使用新的 clz 命令并从中减去它来改进。在 1 ÷ 1 的案例中,这意味着移位将设置为 0,明确地表示它不需要移位。如果它设置移位为负数,表示除数大于被除数,因此我们就可以知道结果是 0,而余数是被除数。我们可以做的另一个快速检查就是,如果当前值为 0,那么它是一个整除的除法,我们就可以停止循环了。

clz dest,src 将第一个寄存器 dest 中二进制表示的值的前导零的数量,保存到第二个寄存器 src 中。
.globl DivideU32
DivideU32:
result .req r0
remainder .req r1
shift .req r2
current .req r3

clz shift,r1
clz r3,r0
subs shift,r3
lsl current,r1,shift
mov remainder,r0
mov result,#0
blt divideU32Return$

divideU32Loop$:
  cmp remainder,current
  blt divideU32LoopContinue$
  
  add result,result,#1
  subs remainder,current
  lsleq result,shift
  beq divideU32Return$
divideU32LoopContinue$:
  subs shift,#1
  lsrge current,#1
  lslge result,#1
  bge divideU32Loop$

divideU32Return$:
.unreq current
mov pc,lr

.unreq result
.unreq remainder
.unreq shift

复制上面的代码到一个名为 maths.s 的文件中。

3、数字字符串

现在,我们已经可以做除法了,我们来看一下另外的一个将数字转换为字符串的实现。下列的伪代码将寄存器中的一个数字转换成以 36 为底的字符串。根据惯例,a % b 表示 a 被 b 相除之后的余数。

function SignedString(r0 is value, r1 is dest, r2 is base)
  if value ≥ 0
  then return UnsignedString(value, dest, base)
  otherwise
    if dest > 0 then
      setByte(dest, '-')
      set dest to dest + 1
    end if
    return UnsignedString(-value, dest, base) + 1
  end if
end function

function UnsignedString(r0 is value, r1 is dest, r2 is base)
  set length to 0
  do
  
    set (value, rem) to DivideU32(value, base)
    if rem &gt 10
    then set rem to rem + '0'
    otherwise set rem to rem - 10 + 'a'
    if dest > 0
    then setByte(dest + length, rem)
    set length to length + 1
  
  while value > 0
  if dest > 0
  then ReverseString(dest, length)
  return length
end function

function ReverseString(r0 is string, r1 is length)
  set end to string + length - 1
  while end > start
    set temp1 to readByte(start)
    set temp2 to readByte(end)
    setByte(start, temp2)
    setByte(end, temp1)
    set start to start + 1
    set end to end - 1
  end while
end function

上述代码实现在一个名为 text.s 的汇编文件中。记住,如果你遇到了困难,可以在下载页面找到完整的解决方案。

4、格式化字符串

我们继续回到我们的字符串格式化方法。因为我们正在编写我们自己的操作系统,我们根据我们自己的意愿来添加或修改格式化规则。我们可以发现,添加一个 a % b 操作去输出一个二进制的数字比较有用,而如果你不使用空终止符字符串,那么你应该去修改 %s 的行为,让它从另一个参数中得到字符串的长度,或者如果你愿意,可以从长度前缀中获取。我在下面的示例中使用了一个空终止符。

实现这个函数的一个主要的障碍是它的参数个数是可变的。根据 ABI 规定,额外的参数在调用方法之前以相反的顺序先推送到栈上。比如,我们使用 8 个参数 1、2、3、4、5、6、7 和 8 来调用我们的方法,我们将按下面的顺序来处理:

  1. 设置 r0 = 5、r1 = 6、r2 = 7、r3 = 8
  2. 推入 {r0,r1,r2,r3}
  3. 设置 r0 = 1、r1 = 2、r2 = 3、r3 = 4
  4. 调用函数
  5. 将 sp 和 #4*4 加起来

现在,我们必须确定我们的函数确切需要的参数。在我的案例中,我将寄存器 r0 用来保存格式化字符串地址,格式化字符串长度则放在寄存器 r1 中,目标字符串地址放在寄存器 r2 中,紧接着是要求的参数列表,从寄存器 r3 开始和像上面描述的那样在栈上继续。如果你想去使用一个空终止符格式化字符串,在寄存器 r1 中的参数将被移除。如果你想有一个最大缓冲区长度,你可以将它保存在寄存器 r3 中。由于有额外的修改,我认为这样修改函数是很有用的,如果目标字符串地址为 0,意味着没有字符串被输出,但如果仍然返回一个精确的长度,意味着能够精确的判断格式化字符串的长度。

如果你希望尝试实现你自己的函数,现在就可以去做了。如果不去实现你自己的,下面我将首先构建方法的伪代码,然后给出实现的汇编代码。

function StringFormat(r0 is format, r1 is formatLength, r2 is dest, ...)
  set index to 0
  set length to 0
  while index < formatLength
    if readByte(format + index) = '%' then
      set index to index + 1
      if readByte(format + index) = '%' then
        if dest > 0
        then setByte(dest + length, '%')
        set length to length + 1
      otherwise if readByte(format + index) = 'c' then
        if dest > 0
        then setByte(dest + length, nextArg)
        set length to length + 1
      otherwise if readByte(format + index) = 'd' or 'i' then
        set length to length + SignedString(nextArg, dest, 10)
      otherwise if readByte(format + index) = 'o' then
        set length to length + UnsignedString(nextArg, dest, 8)
      otherwise if readByte(format + index) = 'u' then
        set length to length + UnsignedString(nextArg, dest, 10)
      otherwise if readByte(format + index) = 'b' then
        set length to length + UnsignedString(nextArg, dest, 2)
      otherwise if readByte(format + index) = 'x' then
        set length to length + UnsignedString(nextArg, dest, 16)
      otherwise if readByte(format + index) = 's' then
        set str to nextArg
        while getByte(str) != '\0'
          if dest > 0
          then setByte(dest + length, getByte(str))
          set length to length + 1
          set str to str + 1
        loop
      otherwise if readByte(format + index) = 'n' then
        setWord(nextArg, length)
      end if
    otherwise
      if dest > 0
      then setByte(dest + length, readByte(format + index))
      set length to length + 1
    end if
    set index to index + 1
  loop
  return length
end function

虽然这个函数很大,但它还是很简单的。大多数的代码都是在检查所有各种条件,每个代码都是很简单的。此外,所有的无符号整数的大小写都是相同的(除了底以外)。因此在汇编中可以将它们汇总。下面是它的汇编代码。

.globl FormatString
FormatString:
format .req r4
formatLength .req r5
dest .req r6
nextArg .req r7
argList .req r8
length .req r9

push {r4,r5,r6,r7,r8,r9,lr}
mov format,r0
mov formatLength,r1
mov dest,r2
mov nextArg,r3
add argList,sp,#7*4
mov length,#0

formatLoop$:
  subs formatLength,#1
  movlt r0,length
  poplt {r4,r5,r6,r7,r8,r9,pc}
  
  ldrb r0,[format]
  add format,#1
  teq r0,#'%'
  beq formatArg$

formatChar$:
  teq dest,#0
  strneb r0,[dest]
  addne dest,#1
  add length,#1
  b formatLoop$

formatArg$:
  subs formatLength,#1
  movlt r0,length
  poplt {r4,r5,r6,r7,r8,r9,pc}

  ldrb r0,[format]
  add format,#1
  teq r0,#'%'
  beq formatChar$
  
  teq r0,#'c'
  moveq r0,nextArg
  ldreq nextArg,[argList]
  addeq argList,#4
  beq formatChar$
  
  teq r0,#'s'
  beq formatString$
  
  teq r0,#'d'
  beq formatSigned$
  
  teq r0,#'u'
  teqne r0,#'x'
  teqne r0,#'b'
  teqne r0,#'o'
  beq formatUnsigned$

  b formatLoop$

formatString$:
  ldrb r0,[nextArg]
  teq r0,#0x0
  ldreq nextArg,[argList]
  addeq argList,#4
  beq formatLoop$
  add length,#1
  teq dest,#0
  strneb r0,[dest]
  addne dest,#1
  add nextArg,#1
  b formatString$

formatSigned$:
  mov r0,nextArg
  ldr nextArg,[argList]
  add argList,#4
  mov r1,dest
  mov r2,#10
  bl SignedString
  teq dest,#0
  addne dest,r0
  add length,r0
  b formatLoop$

formatUnsigned$:
  teq r0,#'u'
  moveq r2,#10
  teq r0,#'x'
  moveq r2,#16
  teq r0,#'b'
  moveq r2,#2
  teq r0,#'o'
  moveq r2,#8
  
  mov r0,nextArg
  ldr nextArg,[argList]
  add argList,#4
  mov r1,dest
  bl UnsignedString
  teq dest,#0
  addne dest,r0
  add length,r0
  b formatLoop$

5、一个转换操作系统

你可以使用这个方法随意转换你希望的任何东西。比如,下面的代码将生成一个换算表,可以做从十进制到二进制到十六进制到八进制以及到 ASCII 的换算操作。

删除 main.s 文件中 bl SetGraphicsAddress 之后的所有代码,然后粘贴以下的代码进去。

mov r4,#0
loop$:
ldr r0,=format
mov r1,#formatEnd-format
ldr r2,=formatEnd
lsr r3,r4,#4
push {r3}
push {r3}
push {r3}
push {r3}
bl FormatString
add sp,#16

mov r1,r0
ldr r0,=formatEnd
mov r2,#0
mov r3,r4

cmp r3,#768-16
subhi r3,#768
addhi r2,#256
cmp r3,#768-16
subhi r3,#768
addhi r2,#256
cmp r3,#768-16
subhi r3,#768
addhi r2,#256

bl DrawString

add r4,#16
b loop$

.section .data
format:
.ascii "%d=0b%b=0x%x=0%o='%c'"
formatEnd:

你能在测试之前推算出将发生什么吗?特别是对于 r3 ≥ 128 会发生什么?尝试在树莓派上运行它,看看你是否猜对了。如果不能正常运行,请查看我们的排错页面。

如果一切顺利,恭喜你!你已经完成了屏幕04 教程,屏幕系列的课程结束了!我们学习了像素和帧缓冲的知识,以及如何将它们应用到树莓派上。我们学习了如何绘制简单的线条,也学习如何绘制字符,以及将数字格式化为文本的宝贵技能。我们现在已经拥有了在一个操作系统上进行图形输出的全部知识。你可以写出更多的绘制方法吗?三维绘图是什么?你能实现一个 24 位帧缓冲吗?能够从命令行上读取帧缓冲的大小吗?

接下来的课程是输入系列课程,它将教我们如何使用键盘和鼠标去实现一个传统的计算机控制台。


via: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/screen04.html

作者:Alex Chadwick 选题:lujun9972 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

屏幕03 课程基于屏幕02 课程来构建,它教你如何绘制文本,和一个操作系统命令行参数上的一个小特性。假设你已经有了课程 7:屏幕02 的操作系统代码,我们将以它为基础来构建。

1、字符串的理论知识

是的,我们的任务是为这个操作系统绘制文本。我们有几个问题需要去处理,最紧急的那个可能是如何去保存文本。令人难以置信的是,文本是迄今为止在计算机上最大的缺陷之一。原本应该是简单的数据类型却导致了操作系统的崩溃,从而削弱其他方面的加密效果,并给使用其它字母表的用户带来了许多问题。尽管如此,它仍然是极其重要的数据类型,因为它将计算机和用户很好地连接起来。文本是计算机能够理解的非常好的结构,同时人类使用它时也有足够的可读性。

那么,文本是如何保存的呢?非常简单,我们使用一种方法,给每个字母分配一个唯一的编号,然后我们保存一系列的这种编号。看起来很容易吧。问题是,那个编号的数量是不固定的。一些文本段可能比其它的长。保存普通数字,我们有一些固有的限制,即:32 位,我们不能超过这个限制,我们要添加方法去使用该长度的数字等等。“文本”这个术语,我们经常也叫它“字符串”,我们希望能够写一个可用于可变长度字符串的函数,否则就需要写很多函数!对于一般的数字来说,这不是个问题,因为只有几种通用的数字格式(字节、字、半字节、双字节)。

可变数据类型(比如文本)要求能够进行很复杂的处理。

因此,如何判断字符串长度?我想显而易见的答案是存储字符串的长度,然后去存储组成字符串的字符。这称为长度前缀,因为长度位于字符串的前面。不幸的是,计算机科学家的先驱们不同意这么做。他们认为使用一个称为空终止符(NULL)的特殊字符(用 \0 表示)来表示字符串结束更有意义。这样确定简化了许多字符串算法,因为你只需要持续操作直到遇到空终止符为止。不幸的是,这成为了许多安全问题的根源。如果一个恶意用户给你一个特别长的字符串会发生什么状况?如果没有足够的空间去保存这个特别长的字符串会发生什么状况?你可以使用一个字符串复制函数来做复制,直到遇到空终止符为止,但是因为字符串特别长,而覆写了你的程序,怎么办?这看上去似乎有些较真,但是,缓冲区溢出攻击还是经常发生。长度前缀可以很容易地缓解这种问题,因为它可以很容易地推算出保存这个字符串所需要的缓冲区的长度。作为一个操作系统开发者,我留下这个问题,由你去决定如何才能更好地存储文本。

缓冲区溢出攻击祸害计算机由来已久。最近,Wii、Xbox 和 Playstation 2、以及大型系统如 Microsoft 的 Web 和数据库服务器,都遭受到缓冲区溢出攻击。

接下来的事情是,我们需要确定的是如何最好地将字符映射到数字。幸运的是,这是高度标准化的,我们有两个主要的选择,Unicode 和 ASCII。Unicode 几乎将每个有用的符号都映射为数字,作为代价,我们需要有很多很多的数字,和一个更复杂的编码方法。ASCII 为每个字符使用一个字节,因此它仅保存拉丁字母、数字、少数符号和少数特殊字符。因此,ASCII 是非常易于实现的,与之相比,Unicode 的每个字符占用的空间并不相同,这使得字符串算法更棘手。通常,操作系统上字符使用 ASCII,并不是为了显示给最终用户的(开发者和专家用户除外),给终端用户显示信息使用 Unicode,因为 Unicode 能够支持像日语字符这样的东西,并且因此可以实现本地化。

幸运的是,在这里我们不需要去做选择,因为它们的前 128 个字符是完全相同的,并且编码也是完全一样的。

表 1.1 ASCII/Unicode 符号 0-127

0123456789abcdef
00NULSOHSTXETXEOTENQACKBELBSHTLFVTFFCRSOSI
10DLEDC1DC2DC3DC4NAKSYNETBCANEMSUBESCFSGSRSUS
20!#$%&.()*+,-./
300123456789:;<=>?
40@ABCDEFGHIJKLMNO
50PQRSTUVWXYZ[\]^\_
60`abcdefghijklmno
70pqrstuvwxyz{ }~DEL

这个表显示了前 128 个符号。一个符号的十六进制表示是行的值加上列的值,比如 A 是 41 16。你可以惊奇地发现前两行和最后的值。这 33 个特殊字符是不可打印字符。事实上,许多人都忽略了它们。它们之所以存在是因为 ASCII 最初设计是基于计算机网络来传输数据的一种方法。因此它要发送的信息不仅仅是符号。你应该学习的重要的特殊字符是 NUL,它就是我们前面提到的空终止符。HT 水平制表符就是我们经常说的 tab,而 LF 换行符用于生成一个新行。你可能想研究和使用其它特殊字符在你的操行系统中的意义。

2、字符

到目前为止,我们已经知道了一些关于字符串的知识,我们可以开始想想它们是如何显示的。为了显示一个字符串,我们需要做的最基础的事情是能够显示一个字符。我们的第一个任务是编写一个 DrawCharacter 函数,给它一个要绘制的字符和一个位置,然后它将这个字符绘制出来。

这就很自然地引出关于字体的讨论。我们已经知道有许多方式去按照选定的字体去显示任何给定的字母。那么字体又是如何工作的呢?在计算机科学的早期阶段,字体就是所有字母的一系列小图片而已,这种字体称为位图字体,而所有的字符绘制方法就是将图片复制到屏幕上。当人们想去调整字体大小时就出问题了。有时我们需要大的字母,而有时我们需要的是小的字母。尽管我们可以为每个字体、每种大小、每个字符都绘制新图片,但这种作法过于单调乏味。所以,发明了矢量字体。矢量字体不包含字体的图像,它包含的是如何去绘制字符的描述,即:一个 o 可能是最大字母高度的一半为半径绘制的圆。现代操作系统都几乎仅使用这种字体,因为这种字体在任何分辨率下都很完美。

在许多操作系统中使用的 TrueType 字体格式是很强大的,它内置有它自己的汇编语言,以确保在任何分辨率下字母看起来都是正确的。

不幸的是,虽然我很想包含一个矢量字体的格式的实现,但它的内容太多了,将占用这个网站的剩余部分。所以,我们将去实现一个位图字体,可是,如果你想去做一个像样的图形操作系统,那么矢量字体将是很有用的。

在下载页面上的字体节中,我们提供了几个 .bin 文件。这些只是字体的原始二进制数据文件。为完成本教程,从等宽、单色、8x16 节中挑选你喜欢的字体。然后下载它并保存到 source 目录中并命名为 font.bin 文件。这些文件只是每个字母的单色图片,它们每个字母刚好是 8 x 16 个像素。所以,每个字母占用 16 字节,第一个字节是第一行,第二个字节是第二行,依此类推。

这个示意图展示了等宽、单色、8x16 的字符 A 的 “Bitstream Vera Sans Mono” 字体。在这个文件中,我们可以找到,它从第 41 16 × 10 16 = 410 16 字节开始的十六进制序列:

00, 00, 00, 10, 28, 28, 28, 44, 44, 7C, C6, 82, 00, 00, 00, 00

在这里我们将使用等宽字体,因为等宽字体的每个字符大小是相同的。不幸的是,大多数字体的复杂之处就是因为它的宽度不同,从而导致它的显示代码更复杂。在下载页面上还包含有几个其它的字体,并包含了这种字体的存储格式介绍。

我们回到正题。复制下列代码到 drawing.s 中的 graphicsAddress.int 0 之后。

.align 4
font:
.incbin "font.bin"
.incbin "file" 插入来自文件 “file” 中的二进制数据。

这段代码复制文件中的字体数据到标签为 font 的地址。我们在这里使用了一个 .align 4 去确保每个字符都是从 16 字节的倍数开始,这是一个以后经常用到的用于加快访问速度的技巧。

现在我们去写绘制字符的方法。我在下面给出了伪代码,你可以尝试自己去实现它。按惯例 >> 的意思是逻辑右移。

function drawCharacter(r0 is character, r1 is x, r2 is y)
  if character > 127 then exit
  set charAddress to font + character × 16
  for row = 0 to 15
  set bits to readByte(charAddress + row)
  for bit = 0 to 7
    if test(bits >> bit, 0x1)
    then setPixel(x + bit, y + row)
    next
  next
  return r0 = 8, r1 = 16
end function

如果直接去实现它,这显然不是个高效率的做法。像绘制字符这样的事情,效率是最重要的。因为我们要频繁使用它。我们来探索一些改善的方法,使其成为最优化的汇编代码。首先,因为我们有一个 × 16,你应该会马上想到它等价于逻辑左移 4 位。紧接着我们有一个变量 row,它只与 charAddressy 相加。所以,我们可以通过增加替代变量来消除它。现在唯一的问题是如何判断我们何时完成。这时,一个很好用的 .align 4 上场了。我们知道,charAddress 将从包含 0 的低位半字节开始。这意味着我们可以通过检查低位半字节来看到进入字符数据的程度。

虽然我们可以消除对 bit 的需求,但我们必须要引入新的变量才能实现,因此最好还是保留它。剩下唯一的改进就是去除嵌套的 bits >> bit

function drawCharacter(r0 is character, r1 is x, r2 is y)
  if character > 127 then exit
  set charAddress to font + character << 4
  loop
    set bits to readByte(charAddress)
    set bit to 8
    loop
      set bits to bits << 1
      set bit to bit - 1
      if test(bits, 0x100)
      then setPixel(x + bit, y)
    until bit = 0
    set y to y + 1
    set chadAddress to chadAddress + 1
  until charAddress AND 0b1111 = 0
  return r0 = 8, r1 = 16
end function

现在,我们已经得到了非常接近汇编代码的代码了,并且代码也是经过优化的。下面就是上述代码用汇编写出来的代码。

.globl DrawCharacter
DrawCharacter:
cmp r0,#127
movhi r0,#0
movhi r1,#0
movhi pc,lr

push {r4,r5,r6,r7,r8,lr}
x .req r4
y .req r5
charAddr .req r6
mov x,r1
mov y,r2
ldr charAddr,=font
add charAddr, r0,lsl #4

lineLoop$:

  bits .req r7
  bit .req r8
  ldrb bits,[charAddr]
  mov bit,#8
  
  charPixelLoop$:
  
    subs bit,#1
    blt charPixelLoopEnd$
    lsl bits,#1
    tst bits,#0x100
    beq charPixelLoop$
    
    add r0,x,bit
    mov r1,y
    bl DrawPixel
    
    teq bit,#0
    bne charPixelLoop$
  
  charPixelLoopEnd$:
  .unreq bit
  .unreq bits
  add y,#1
  add charAddr,#1
  tst charAddr,#0b1111
  bne lineLoop$

.unreq x
.unreq y
.unreq charAddr

width .req r0
height .req r1
mov width,#8
mov height,#16

pop {r4,r5,r6,r7,r8,pc}
.unreq width
.unreq height

3、字符串

现在,我们可以绘制字符了,我们可以绘制文本了。我们需要去写一个方法,给它一个字符串为输入,它通过递增位置来绘制出每个字符。为了做的更好,我们应该去实现新的行和制表符。是时候决定关于空终止符的问题了,如果你想让你的操作系统使用它们,可以按需来修改下面的代码。为避免这个问题,我将给 DrawString 函数传递一个字符串长度,以及字符串的地址,和 xy 的坐标作为参数。

function drawString(r0 is string, r1 is length, r2 is x, r3 is y)
  set x0 to x
  for pos = 0 to length - 1
    set char to loadByte(string + pos)
    set (cwidth, cheight) to DrawCharacter(char, x, y)
    if char = '\n' then
      set x to x0
      set y to y + cheight
    otherwise if char = '\t' then
      set x1 to x
      until x1 > x0
        set x1 to x1 + 5 × cwidth
      loop
    set x to x1
    otherwise
      set x to x + cwidth
    end if
  next
end function

同样,这个函数与汇编代码还有很大的差距。你可以随意去尝试实现它,即可以直接实现它,也可以简化它。我在下面给出了简化后的函数和汇编代码。

很明显,写这个函数的人并不很有效率(感到奇怪吗?它就是我写的)。再说一次,我们有一个 pos 变量,它用于递增及与其它东西相加,这是完全没有必要的。我们可以去掉它,而同时进行长度递减,直到减到 0 为止,这样就少用了一个寄存器。除了那个烦人的乘以 5 以外,函数的其余部分还不错。在这里要做的一个重要事情是,将乘法移到循环外面;即便使用位移运算,乘法仍然是很慢的,由于我们总是加一个乘以 5 的相同的常数,因此没有必要重新计算它。实际上,在汇编代码中它可以在一个操作数中通过参数移位来实现,因此我将代码改变为下面这样。

function drawString(r0 is string, r1 is length, r2 is x, r3 is y)
  set x0 to x
  until length = 0
    set length to length - 1
    set char to loadByte(string)
    set (cwidth, cheight) to DrawCharacter(char, x, y)
    if char = '\n' then
      set x to x0
      set y to y + cheight
    otherwise if char = '\t' then
      set x1 to x
      set cwidth to cwidth + cwidth << 2
      until x1 > x0
        set x1 to x1 + cwidth
      loop
      set x to x1
    otherwise
      set x to x + cwidth
    end if
    set string to string + 1
  loop
end function

以下是它的汇编代码:

.globl DrawString
DrawString:
x .req r4
y .req r5
x0 .req r6
string .req r7
length .req r8
char .req r9
push {r4,r5,r6,r7,r8,r9,lr}

mov string,r0
mov x,r2
mov x0,x
mov y,r3
mov length,r1

stringLoop$:
  subs length,#1
  blt stringLoopEnd$
  
  ldrb char,[string]
  add string,#1
  
  mov r0,char
  mov r1,x
  mov r2,y
  bl DrawCharacter
  cwidth .req r0
  cheight .req r1
  
  teq char,#'\n'
  moveq x,x0
  addeq y,cheight
  beq stringLoop$
  
  teq char,#'\t'
  addne x,cwidth
  bne stringLoop$
  
  add cwidth, cwidth,lsl #2
  x1 .req r1
  mov x1,x0
  
  stringLoopTab$:
    add x1,cwidth
    cmp x,x1
    bge stringLoopTab$
  mov x,x1
  .unreq x1
  b stringLoop$
stringLoopEnd$:
.unreq cwidth
.unreq cheight

pop {r4,r5,r6,r7,r8,r9,pc}
.unreq x
.unreq y
.unreq x0
.unreq string
.unreq length

这个代码中非常聪明地使用了一个新运算,subs 是从一个操作数中减去另一个数,保存结果,然后将结果与 0 进行比较。实现上,所有的比较都可以实现为减法后的结果与 0 进行比较,但是结果通常会丢弃。这意味着这个操作与 cmp 一样快。

subs reg,#val 从寄存器 reg 中减去 val,然后将结果与 0 进行比较。

4、你的意愿是我的命令行

现在,我们可以输出字符串了,而挑战是找到一个有意思的字符串去绘制。一般在这样的教程中,人们都希望去绘制 “Hello World!”,但是到目前为止,虽然我们已经能做到了,我觉得这有点“君临天下”的感觉(如果喜欢这种感觉,请随意!)。因此,作为替代,我们去继续绘制我们的命令行。

有一个限制是我们所做的操作系统是用在 ARM 架构的计算机上。最关键的是,在它们引导时,给它一些信息告诉它有哪些可用资源。几乎所有的处理器都有某些方式来确定这些信息,而在 ARM 上,它是通过位于地址 100 16 处的数据来确定的,这个数据的格式如下:

1. 数据是可分解的一系列的标签。
2. 这里有九种类型的标签:`core`、`mem`、`videotext`、`ramdisk`、`initrd2`、`serial`、`revision`、`videolfb`、`cmdline`。
3. 每个标签只能出现一次,除了 `core` 标签是必不可少的之外,其它的都是可有可无的。
4. 所有标签都依次放置在地址 `0x100` 处。
5. 标签列表的结束处总是有两个<ruby>字<rt>word</rt></ruby>,它们全为 0。
6. 每个标签的字节数都是 4 的倍数。
7. 每个标签都是以标签中(以字为单位)的标签大小开始(标签包含这个数字)。
8. 紧接着是包含标签编号的一个半字。编号是按上面列出的顺序,从 1 开始(`core` 是 1,`cmdline` 是 9)。
9. 紧接着是一个包含 5441<sub>16</sub> 的半字。
10. 之后是标签的数据,它根据标签不同是可变的。数据大小(以字为单位)+ 2 的和总是与前面提到的长度相同。
11. 一个 `core` 标签的长度可以是 2 个字也可以是 5 个字。如果是 2 个字,表示没有数据,如果是 5 个字,表示它有 3 个字的数据。
12. 一个 `mem` 标签总是 4 个字的长度。数据是内存块的第一个地址,和内存块的长度。
13. 一个 `cmdline` 标签包含一个 `null` 终止符字符串,它是个内核参数。

在目前的树莓派版本中,只提供了 corememcmdline 标签。你可以在后面找到它们的用法,更全面的参考资料在树莓派的参考页面上。现在,我们感兴趣的是 cmdline 标签,因为它包含一个字符串。我们继续写一些搜索这个命令行(cmdline)标签的代码,如果找到了,以每个条目一个新行的形式输出它。命令行只是图形处理器或用户认为操作系统应该知道的东西的一个列表。在树莓派上,这包含了 MAC 地址、序列号和屏幕分辨率。字符串本身也是一个由空格隔开的表达式(像 key.subkey=value 这样的)的列表。

几乎所有的操作系统都支持一个“命令行”的程序。它的想法是为选择一个程序所期望的行为而提供一个通用的机制。

我们从查找 cmdline 标签开始。将下列的代码复制到一个名为 tags.s 的新文件中。

.section .data
tag_core: .int 0
tag_mem: .int 0
tag_videotext: .int 0
tag_ramdisk: .int 0
tag_initrd2: .int 0
tag_serial: .int 0
tag_revision: .int 0
tag_videolfb: .int 0
tag_cmdline: .int 0

通过标签列表来查找是一个很慢的操作,因为这涉及到许多内存访问。因此,我们只想做一次。代码创建一些数据,用于保存每个类型的第一个标签的内存地址。接下来,用下面的伪代码就可以找到一个标签了。

function FindTag(r0 is tag)
  if tag > 9 or tag = 0 then return 0
  set tagAddr to loadWord(tag_core + (tag - 1) × 4)
  if not tagAddr = 0 then return tagAddr
  if readWord(tag_core) = 0 then return 0
  set tagAddr to 0x100
  loop forever
    set tagIndex to readHalfWord(tagAddr + 4)
    if tagIndex = 0 then return FindTag(tag)
    if readWord(tag_core+(tagIndex-1)×4) = 0
    then storeWord(tagAddr, tag_core+(tagIndex-1)×4)
    set tagAddr to tagAddr + loadWord(tagAddr) × 4
  end loop
end function

这段代码已经是优化过的,并且很接近汇编了。它尝试直接加载标签,第一次这样做是有些乐观的,但是除了第一次之外的其它所有情况都是可以这样做的。如果失败了,它将去检查 core 标签是否有地址。因为 core 标签是必不可少的,如果它没有地址,唯一可能的原因就是它不存在。如果它有地址,那就是我们没有找到我们要找的标签。如果没有找到,那我们就需要查找所有标签的地址。这是通过读取标签编号来做的。如果标签编号为 0,意味着已经到了标签列表的结束位置。这意味着我们已经查找了目录中所有的标签。所以,如果我们再次运行我们的函数,现在它应该能够给出一个答案。如果标签编号不为 0,我们检查这个标签类型是否已经有一个地址。如果没有,我们在目录中保存这个标签的地址。然后增加这个标签的长度(以字节为单位)到标签地址中,然后去查找下一个标签。

尝试去用汇编实现这段代码。你将需要简化它。如果被卡住了,下面是我的答案。不要忘了 .section .text

.section .text
.globl FindTag
FindTag:
tag .req r0
tagList .req r1
tagAddr .req r2

sub tag,#1
cmp tag,#8
movhi tag,#0
movhi pc,lr

ldr tagList,=tag_core
tagReturn$:
add tagAddr,tagList, tag,lsl #2
ldr tagAddr,[tagAddr]

teq tagAddr,#0
movne r0,tagAddr
movne pc,lr

ldr tagAddr,[tagList]
teq tagAddr,#0
movne r0,#0
movne pc,lr

mov tagAddr,#0x100
push {r4}
tagIndex .req r3
oldAddr .req r4
tagLoop$:
ldrh tagIndex,[tagAddr,#4]
subs tagIndex,#1
poplt {r4}
blt tagReturn$

add tagIndex,tagList, tagIndex,lsl #2
ldr oldAddr,[tagIndex]
teq oldAddr,#0
.unreq oldAddr
streq tagAddr,[tagIndex]

ldr tagIndex,[tagAddr]
add tagAddr, tagIndex,lsl #2
b tagLoop$

.unreq tag
.unreq tagList
.unreq tagAddr
.unreq tagIndex

5、Hello World

现在,我们已经万事俱备了,我们可以去绘制我们的第一个字符串了。在 main.s 文件中删除 bl SetGraphicsAddress 之后的所有代码,然后将下面的代码放进去:

mov r0,#9
bl FindTag
ldr r1,[r0]
lsl r1,#2
sub r1,#8
add r0,#8
mov r2,#0
mov r3,#0
bl DrawString
loop$:
b loop$

这段代码简单地使用了我们的 FindTag 方法去查找第 9 个标签(cmdline),然后计算它的长度,然后传递命令和长度给 DrawString 方法,告诉它在 0,0 处绘制字符串。现在可以在树莓派上测试它了。你应该会在屏幕上看到一行文本。如果没有,请查看我们的排错页面。

如果一切正常,恭喜你已经能够绘制文本了。但它还有很大的改进空间。如果想去写了一个数字,或内存的一部分,或操作我们的命令行,该怎么做呢?在 课程 9:屏幕04 中,我们将学习如何操作文本和显示有用的数字和信息。


via: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/screen03.html

作者:Alex Chadwick 选题:lujun9972 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出