本节将继续学习逻辑指令(and、or、xor)和移位指令(sll、srl、sra)

逻辑指令

从CPU芯片电路角度来看,其实CPU更擅长指令逻辑操作,如与、或、异或

RISC-V指令集中包含了三种逻辑指令,这些指令又分为立即数版本寄存器版本,分别是andi、and、ori、or、xori、xor这六条指令。

按位与操作:andi、and指令

andi、and 指令,它们的形式如下所示:

andi rd,rs1,imm
#andi 立即数按位与指令
#rd 目标寄存器
#rs1 源寄存器1
#imm 立即数
and rd,rs1,rs2
#and 寄存器按位与指令
#rd 目标寄存器
#rs1 源寄存器1
#rs2 源寄存器2

上述代码中 rd、rs1、rs2 可以是任何通用寄存器,imm 是立即数。

andi、and 这两个指令完成的操作,我们用伪代码描述如下:

//andi
rd = rs1 & imm
//and
rd = rs1 & rs2

按位与的操作,就是把 rs1 与 imm 或者 rs1 与 rs2 其中的每个数据位两两相与。两个位都是 1,结果为 1,否则结果为 0。

下面我们在工程目录下建立一个 and.S 文件,写代码验证一下这两个指令,如下所示:

.globl andi_ins
andi_ins:
andi a0,a0,0xff #a0 = a0&0xff,a0是C语言调用者传递的参数,a0也是返回值,这样计算结果就返回了
jr ra #函数返回 .globl and_ins
and_ins:
and a0,a0,a1 #a0 = a0&a1,a0、a1是C语言调用者传递的参数,a0是返回值,这样计算结果就返回了
jr ra #函数返回

andi 指令是拿** a0 寄存器和立即数 0xff 进行与操作。由于立即数是 0xff,所以总是返回 a0 的低 8 位数据;and 指令则是拿 a0 和 a1 寄存器进行与操作,再把结果写入到 a0 寄存器**。

用 VSCode 打开工程按下“F5”调试一下,如下所示:

上图中是执行完 andi a0,a0,0xff 指令之后,执行 jr ra 指令之前的状态。可以看到,a0 寄存器中的值确实已经变成 2 了,这说明运算的结果是符合预期的。

andi_ins 函数返回后,输出的结果如下图所示:



因为 2 的二进制数据是(0b00000000000000000000000000000010)与上 0xff 的二进制数据是(0b00000000000000000000000011111111)结果确实是 2,所以返回 2,结果是正确的。

接下来,我们对 and_ins 函数进行调试。

上图展示的是执行完 and a0,a0,a1 指令之后,执行 jr ra 指令之前的状态。我们看到 a0 寄存器中的值已经变成了 1,这说明运算的结果是正确的。

and_ins 函数返回后,输出的结果如下图所示:

上图中因为 1 的二进制数据是(0b00000000000000000000000000000001)与上 1 的二进制数据是(0b00000000000000000000000000000001)确实是 1,所以返回 1,结果完全正确。

按位或操作:ori、or指令

或指令 ori、or,它们的形式如下:

ori rd,rs1,imm
#ori 立即数按位或指令
#rd 目标寄存器
#rs1 源寄存器1
#imm 立即数
or rd,rs1,rs2
#or 寄存器按位或指令
#rd 目标寄存器
#rs1 源寄存器1
#rs2 源寄存器2

同样地,上述代码中 rd、rs1、rs2 可以是任何通用寄存器,imm 表示立即数。

我们还是从伪代码的描述入手,看看 ori、or 完成的操作。

//ori
rd = rs1 | imm
//or
rd = rs1 | rs2

按位或的操作就是把 rs1 与 imm 或者 rs1 与 rs2 其中的每个数据位两两相或,两个位有一位为 1,结果为 1,否则结果为 0。

在 and.S 文件中写写代码,做个验证,如下所示:

.globl ori_ins
ori_ins:
ori a0,a0,0 #a0 = a0|0,a0是C语言调用者传递的参数,a0也是返回值,这样计算结果就返回了
jr ra #函数返回 .globl or_ins
or_ins:
or a0,a0,a1 #a0 = a0|a1,a0、a1是C语言调用者传递的参数,a0是返回值,这样计算结果就返回了
jr ra #函数返回

上述代码中 ori_ins 与 or_ins 函数,分别执行了 ori 和 or 指令。

ori 指令是拿 a0 寄存器和立即数 0 进行或操作,由于立即数是 0,所以总是返回 a0 原本的数据;or 指令是拿 a0 和 a1 寄存器进行或操作,再把结果写入到 a0 寄存器。

VSCode 里,按下“F5”调试一下,如下所示:



上图中是执行完 ori a0,a0,0 指令之后,执行 jr ra 指令之前的状态。如果 a0 寄存器中的值确实已经变成 0xf0f0 了,就说明运算的结果正确。

ori_ins 函数返回后,输出的结果如下图所示:

因为 0xf0f0 的二进制数据是(0b00000000000000001111000011110000)或上 0 的二进制数据是(0b00000000000000000000000000000000)按位或操作是“有 1 为 1”,所以返回 0xf0f0,结果是正确的。

再用同样的方法调试一下 or_ins 函数,如下图所示:

上图展示的是执行完 or a0,a0,a1 指令之后,执行 jr ra 指令之前的状态。如果我们看到 a0 寄存器中的值确实已经变成 0x1111 了,就说明运算的结果正确,符合预期。

or_ins 函数返回后,输出的结果如下:



上图中 or_ins 函数第一个参数为 0x1000 的二进制数据是(0b00000000000000000001000000000000)第二个参数为 0x1111 的二进制数据是(0b00000000000000000001000100010001)两个参数相或,而按位或操作是“有 1 为 1”,所以返回 0x1111,结果是正确的。

按位异或操作:xori、xor 指令

逻辑指令中的最后两条指令 xori、xor,即异或指令的立即数版本和寄存器版本,它们的形式如下所示:

xori rd,rs1,imm
#xori 立即数按位异或指令
#rd 目标寄存器
#rs1 源寄存器1
#imm 立即数
xor rd,rs1,rs2
#xor 寄存器按位异或指令
#rd 目标寄存器
#rs1 源寄存器1
#rs2 源寄存器2

xori、xor 完成的操作用伪代码描述如下:

//xori
rd = rs1 ^ imm
//xor
rd = rs1 ^ rs2

按位异或的操作是把 rs1 与 imm 或者 rs1 与 rs2 其中的每个数据位两两相异或,两个位如果不相同,结果为 1。如果两个位相同,结果为 0。

在 and.S 文件中写代码验证一下,如下所示。

.globl xori_ins
xori_ins:
xori a0,a0,0 #a0 = a0^0,a0是C语言调用者传递的参数,a0也是返回值,这样计算结果就返回了
jr ra #函数返回 .globl xor_ins
xor_ins:
xor a0,a0,a1 #a0 = a0^a1,a0、a1是C语言调用者传递的参数,a0是返回值,这样计算结果就返回了
jr ra #函数返回

xori 指令是拿 a0 寄存器和立即数 0 进行异或操作,由于立即数是 0,而且各个数据位相同为 0,不同为 1,所以同样会返回 a0 原本的数据 ;而 xor 指令是拿 a0 和 a1 寄存器进行或操作,再把结果写入到 a0 寄存器。

按下“F5”调试一下,如下所示

上图中是执行完 xori a0,a0,0 指令之后,执行 jr ra 指令之前的状态,我们已经看到 a0 寄存器中的值已经变成 0xff 了,这说明运算的结果正确。

xori_ins 函数返回后,输出的结果如下图所示:



结合上面这张截图不难发现,我们传递给 xori_ins 函数的参数是 0xff,因为 0xff 的二进制数据是(0b00000000000000000000000011111111)异或上 0 的二进制数据是(0b00000000000000000000000000000000)按位异或操作是“相同为 0,不同为 1”,所以返回 0xff,结果是正确的。

再来调试一下 xor_ins 函数。xor a0,a0,a1 指令执行完成之后,执行 jr ra 指令之前的状态如图所示:

看到 a0 寄存器中的值已经变成 0 了,这说明运算的结果正确,符合预期。

xor_ins 函数返回后,输出的结果如下图所示:



由于我们给 xor_ins 函数传递了两个相同的参数都是 0xffff。因为 0xffff 的二进制数据是(0b00000000000000001111111111111111)两者异或,按位异或操作是“相同为 0,不同为 1”,所以返回 0,结果是正确的。

下面来看下andi、and、ori、or、xori、xor 这六条指令的二进制数据。

打开工程目录下的 and.bin 文件,如下所示:



上述图中的 12 个 32 位数据是 12 条指令,其中六个 0x00008067 数据是六个函数的返回指令。

具体的指令形式,还有对应的汇编语句,见下表格:



同样地,我带你拆分一下 andi、and、ori、or、xori、xor 指令的各位段的数据,看看它们是如何编码的。

从上图中可以发现,立即数版本和寄存器版本的 and、or、xor 指令通过操作码区分,而它们之间的寄存器和立即数版本是靠功能位段来区分,立即数位段和源寄存器与目标寄存器位段和之前的指令是相同的。

移位指令

移位指令和逻辑操作指令一样,都是 CPU 电路很容易就能实现的。

RISC-V 指令集中的移位指令包括逻辑左移、逻辑右移和算术右移,它们分别有立即数和寄存器版本,所以一共有六条。

逻辑左移指令:slli、sll指令

slli(Shift Left Logical Immediate)

先看看逻辑左移指令,也就是 slli、sll 指令,它们的形式如下所示:

slli rd,rs1,imm
#slli 立即数逻辑左移指令
#rd 目标寄存器
#rs1 源寄存器1
#imm 立即数,rs1左移的位数,0~31
sll rd,rs1,rs2
#sll 寄存器逻辑左移指令
#rd 目标寄存器
#rs1 源寄存器1
#rs2 源寄存器2,rs1左移的位数

上述代码中 rd、rs1、rs2 可以是任何通用寄存器。imm 是立即数,其实在官方文档中,这里是 shamt,表示 rs1 左移 shamt 位。这里我为了和之前的形式保持一致,才继续沿用了 imm。

slli、sll 它们俩完成的操作,用伪代码描述如下:

//slli
rd = rs1 << imm
//sll
rd = rs1 << rs2

逻辑左移的操作是把 rs1 中的数据向左移动 imm 位,或者把 rs1 中的数据向左移动 rs2 位,右边多出的空位填 0 并写入 rd 中。

在工程目录下,建立一个 sll.S 文件,写代码验证一下,如下所示

.globl slli_ins
slli_ins:
slli a0, a0, 4 #a0 = a0<<4,a0是C语言调用者传递的参数,a0也是返回值,这样计算结果就返回了
jr ra #函数返回 .globl sll_ins
sll_ins:
sll a0, a0, a1 #a0 = a0<<a1,a0、a1是C语言调用者传递的参数,a0是返回值,这样计算结果就返回了
jr ra #函数返回

立即数逻辑左移 slli 指令是把 a0 中的数据左移 4 位。而逻辑左移 sll 指令是把 a0 中的数据左移,左移多少位要取决于 a1 中的数据,完成移动后再把结果写入到 a0 寄存器。

用 VSCode 打开工程,按下“F5”调试,如下所示:

上图中是进入 slli_ins 函数,执行完 slli a0,a0,4 指令之后,执行 jr ra 指令之前的状态,我们给 slli_ins 函数传进来的参数是 0xffff。现在对照图示就能看到,a0 寄存器中的值确实已经变成 0xffff0 了,这说明运算结果是正确的。

slli_ins 函数返回后,输出的结果如下:

因为 0xffff 二进制数据是(0b00000000000000001111111111111111),逻辑左移 4 位后的结果是 0xffff0,它的二进制数据是(0b00000000000011111111111111110000),结果正确无误。

下面我们接着对 sll_ins 函数进行调试,如下所示:

上图中是进入 sll_ins 函数,执行完 sll a0,a0,a1 指令之后,执行 jr ra 指令之前的状态,我们给 sll_ins 函数传进来的参数是 0xeeeeeeee 和 31(a1 寄存器)。如果看到 a0 寄存器中的值确实已经变成 0 了,这说明运算结果是正确的。

sll_ins 函数返回后,输出的结果如下图所示:

第一个参数 0xeeeeeeee 的二进制数据是(0b11101110111011101110111011101110),逻辑左移 31 位后的结果是 0,因为它把所有的二进制数据位都移出去了,然后空位补 0,所以结果正确无误。

逻辑右移指令:srli、srl

有逻辑左移就有逻辑右移。逻辑右移指令 srli、srl,分别对应着立即数和寄存器版本,它们的形式如下:

srli rd,rs1,imm
#srli 立即数逻辑右移指令
#rd 目标寄存器
#rs1 源寄存器1
#imm 立即数,rs1右移的位数,0~31
srl rd,rs1,rs2
#srl 寄存器逻辑右移指令
#rd 目标寄存器
#rs1 源寄存器1
#rs2 源寄存器2,rs1右移的位数

上述代码中 rd、rs1、rs2 可以是任何通用寄存器。imm 是立即数。为了和之前的形式保持一致,我们还是沿用 imm,而非官方文档中的 shamt。

srli、srl 完成的操作,可以用后面的伪代码来描述:

//srli
rd = rs1 >> imm
//srl
rd = rs1 >> rs2

逻辑右移的操作是把 rs1 中的数据向右移动 imm 位。或者把 rs1 中的数据向右移动 rs2 位,左边多出的空位填 0 并写入 rd 中。

在 sll.S 文件中写段代码来验证一下,如下所示:

.globl srli_ins
srli_ins:
srli a0, a0, 8 #a0 = a0>>8,a0是C语言调用者传递的参数,a0也是返回值,这样计算结果就返回了
jr ra #函数返回 .globl srl_ins
srl_ins:
srl a0, a0, a1 #a0 = a0>>a1,a0、a1是C语言调用者传递的参数,a0是返回值,这样计算结果就返回了
jr ra #函数返回

代码中立即数逻辑右移 srli 指令是把 a0 中的数据右移 8 位。逻辑右移 srl 指令,则是把 a0 中的数据右移,右移多少位要看 a1 中数据表示的位数是多少,再把结果写入到 a0 寄存器。

打开工程按下“F5”就可以调试了,效果如图:



上图中是进入 srli_ins 函数,执行完 srli a0,a0,8 指令之后,执行 jr ra 指令之前的状态,我们给 srli_ins 函数传进来的参数是 0xffff。现在,对照截图可以看到 a0 寄存器中的值确实已经变成 0xff 了,这说明运算结果正确。

srli_ins 函数返回后,输出的结果如下图所示:

因为调用函数 srli_ins 的参数 0xffff 的二进制数据是(0b00000000000000001111111111111111),逻辑右移 8 位后的结果是 0xff,它的二进制数据是(0b00000000000000000000000011111111),结果正确,符合我们的预期

拿下了 srli_ins 函数,接下来就是 srl_ins 函数的调试,如下所示:

上图中是调用进入 srl_ins 函数,执行完 srl a0,a0,a1 指令之后,执行 jr ra 指令之前的状态,给 srl_ins 函数传进来的参数是 0xaaaaaaaa。可以看到,a0 寄存器中的值确实已经变成 0xaaaa 了,所以运算结果也是正确的。

srl_ins 函数返回后,输出的结果如下图所示:



给 srl_ins 函数传进来的第一个参数是 0xaaaaaaaa 的二进制数据是(0b10101010101010101010101010101010),逻辑右移 16 位后的结果是 0xaaaa,其二进制数据为(0b00000000000000001010101010101010 ),因为它把低 16 位二进制数据位移出去了,然后高 16 位的空位补 0,所以结果正确无误。

算术右移指令:srai、sra

最后还有两个算术右移指令,它们和逻辑右移的最大区别是,数据在逻辑右移之后左边多出空位用 0 填充,而数据在算术右移之后左边多出的空位是用数据的符号位填充。如果数据的符号位为 1 就填充 1,如果为 0 就填充 0

它们的形式和伪代码与逻辑右移是一样的,只不过指令助记符由 srli、srl,变成了 srai、sra。

直接在 sll.S 文件中,写代码进行验证。

.globl srai_ins
srai_ins:
srai a0, a0, 8 #a0 = a0>>8,a0是C语言调用者传递的参数,a0也是返回值,这样计算结果就返回了
jr ra #函数返回 .globl sra_ins
sra_ins:
sra a0, a0, a1 #a0 = a0>>a1,a0、a1是C语言调用者传递的参数,a0是返回值,这样计算结果就返回了
jr ra #函数返回

上述代码中的两个函数 srai_ins 与 sra_ins,可以实现算术右移。先看立即数算术右移 srai 指令,它把 a0 中的数据右移了 8 位。而算术右移 srl 指令是把 a0 中的数据右移,右移多少位由 a1 中的数据表示的位数来决定,之后再把结果写入到 a0 寄存器。

按下“F5”,调试的结果如下:



上图中是进入立即数算术右移函数 srai_ins,执行完 srai a0,a0,8 指令之后,执行 jr ra 指令之前的状态。对照图里红框的内容可以看到,给 srai_ins 函数传进来的参数是 0x1111。如果 a0 寄存器中的值确实已经变成 0x11 了,就代表运算结果正确。

srai_ins 函数返回后,输出的结果如下:

因为我们给立即数算术右移函数 srai_ins 的参数 0x1111,其二进制数据是(0b00000000000000000001000100010001),符号位为 0,所以算术右移 8 位后的结果是 0x11,它的二进制数据是(0b00000000000000000000000000010001),结果非常正确。

接着调试一下 sra_ins 函数,如下所示:

上图中是进入算术右移函数 sra_ins,执行完 sra a0,a0,a1 指令之后,执行 jr ra 指令之前的状态。对照图里左侧红框的部分,我们就能知道 sra_ins 函数传进来的参数是 0xaaaaaaaa,你可能判断 a0 寄存器里输出的结果应该是 0x0000aaaa,但调试显示的实际结果却是 0xffffaaaa。

先看看 sra_ins 函数返回后输出的结果是什么,然后再分析原因。



因为我们给算术右移函数 sra_ins 的参数是 0xaaaaaaaa 和 16,这表明对 0xaaaaaaaa 算术右移 16,0xaaaaaaaa 的二进制数据是(0b10101010101010101010101010101010),注意其符号位为 1,所以算术右移 16 位后的结果是 0xffffaaaa,它的二进制数据是(0b11111111111111111010101010101010),结果是符合预期的。输出的结果也证实了这一点。

还是要看一下 slli、sll、srli、srl、srai、sra 这六条指令的二进制数据,我们打开工程目录下的 sll.bin 文件。

可以看出,图中的 12 个 32 位数据是 12 条指令,其中六个 0x00008067 数据是六个函数的返回指令。具体的指令形式,还有对应的汇编语句,你可以参考后面的表格。



我们拆分一下 slli、sll、srli、srl、srai、sra 指令的各位段的数据,看看它们是在内存中如何编码的,你可以结合示意图来理解。

从上图中我们可以发现,sll、srl、sra 指令的立即数版本和寄存器版本要通过操作码区分,而它们之间是靠功能位段来区分的,源寄存器与目标寄存器所在的位段和之前的指令是相同的。需要注意的是,这些立即数版本的立即数位段在官方文档中叫 shamt 位段,并且只占 5 位,而其它指令的立即数占 12 位,这里为了一致性还是沿用立即数。

参考:

RISC-V指令精讲(二):算术指令实现与调试

RISC-V指令:逻辑指令与移位指令的更多相关文章

  1. ARM指令分类学习

    指令分类: 1.算数和逻辑指令 2.比较指令 3.跳转指令 4.移位指令 5.程序状态字访问指令 6.存储器访问指令 +++++++++++++++++++++++++++++++++++++++++ ...

  2. 计算机系统6-> 计组与体系结构3 | MIPS指令集(中)| MIPS汇编指令与机器表示

    上一篇计算机系统5-> 计组与体系结构2 | MIPS指令集(上)| 指令系统从顶层讲解了一个指令集 / 指令系统应当具备哪些特征和工作原理.这一篇就聚焦MIPS指令集(MIPS32),看看其汇 ...

  3. RV32I基础整数指令集

    RV32I是32位基础整数指令集,它支持32位寻址空间,支持字节地址访问,仅支持小端格式(little-endian,高地址高位,低地址地位),寄存器也是32位整数寄存器.RV32I指令集的目的是尽量 ...

  4. 常见的CPU指令集介绍

    本文摘自网络   一.X86 是微处理器执行的计算机语言指令集,指一个intel通用计算机系列的标准编号缩写,也标识一套通用的计算机指令集合,属于CISC. 1.1.简介 X86指令集是美国Intel ...

  5. Cracking Digital VLSI Verification Interview 第二章

    Computer Architecture 对Cracking Digital VLSI Verification Interview:Interview Success这本书的汉化,最新更新请关注微 ...

  6. 北大2022编译原理实践(C/C++)-sysy 编译器构建

    这是今年新推出的实践方案,由往年的sysy->IR1->IR2->RISC V变成了sysy->Koopa->RISC V,通过增量的方式让整个实践过程更容易上手 所以先 ...

  7. 【Maven】搭建Maven环境

    第一步:下载,并安装配置Maven 下载安装包:可以到官网下载(可能很慢),建议从CSDN上下载. 解压安装包:解压到Eclipse和Java一起把,改配置什么的一眼就看到:反正我是把Java,Ecl ...

  8. Maven安装最佳实践(Windows平台)

    第一步:下载maven,解压缩. 在maven官网下载maven文件,这里我下载的是"apache-maven-2.2.1-bin.zip",如果需要maven的源代码,可以选择下 ...

  9. CPU介绍

    CPU内核主要分为两部分:运算器和控制器. (一) 运算器 cpu基本想到的是计算,因此有算数计算,还有逻辑计算单元以及移位简单的运算:fp运算单独拿出:要运算就需要输入数字,因此有寄存器组,即通用寄 ...

  10. Perl语言——简单说明

    Perl语言——简单说明 一.简单说明 Perl语言全称:实用摘录与报表语言|病态折中式垃圾列表器.Perl名称并不是缩写词,而是个溯写字. Perl语言历史:Larry Wall(拉里·沃尔)20世 ...

随机推荐

  1. keil优化等级说明 keil code optimization

    其中 0级(Constan folding)的优化包括: a.常数折叠:只要有可能,编译器就执行将表达式化为常数数字的计算,其中包括运行地址的计算. b. 简单访问优化:对8051系统的内部数据和位地 ...

  2. 批量上传序列到Gisaid数据库

    本文演示的是用Gisaid官网自带的Excel宏批量上传毒株信息和序列到Gisaid数据库,需要魔法. 1. 下载GISAID Batch Uploader 找到自己需要上传的病毒类型,我是Flu,在 ...

  3. 【FAQ】HarmonyOS SDK 闭源开放能力 —Map Kit(5)

    1.问题描述: 提供两套标准方案,可根据体验需求选择: 1.地图Picker(地点详情) 用户体验:①展示地图 ②标记地点 ③用户选择已安装地图应用 接入文档:https://developer.hu ...

  4. Python 潮流周刊#92:一张 Python 软件包的地图(摘要)

    本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章.教程.开源项目.软件工具.播客和视频.热门话题等内容.愿景:帮助所有读者精进 Python 技术,并增长职 ...

  5. sap 管理--企业解决方案 -设备管理

    1.什么是sap 管理 2.设备管理管的是什么 3.设备的几种状态 4.设备bom(物料清单) 5.测量点计数器 1.什么是sap 管理 System Applications and Product ...

  6. k8s报错Error: template: nvidia-device-plugin/templates/gfd.yml:22:19: executing "nvidia-device-plugin/templates/gfd.yml" at <.Subcharts.gfd>: nil pointer evaluating interface {}.gfd

    前言 在安装 kubernetes 插件 k8s-device-plugin时,报错: Error: template: nvidia-device-plugin/templates/gfd.yml: ...

  7. NumPy学习8

    今天学习了NumPy统计函数 16,NumPy统计函数 numpy_test8.py : import numpy as np ''' 16,NumPy统计函数 NumPy 提供了许多统计功能的函数, ...

  8. 【Git】GitHub

    GitHub 本地库与远程库开发模式 开发模式一:团队内部协作 项目经理岳不群,程序员令狐冲 岳不群把他的本地库推送到远程库 令狐冲克隆远程库到自己的本地库 令狐冲在自己本地库的基础上修改代码,提交到 ...

  9. 流式计算(四)-Flink Stream API 篇二

    个人原创文章,禁止任何形式转载,否则追究法律责任! 本文只发表在"公众号"和"博客园",其他均属复制粘贴!如果觉得排版不清晰,请查看公众号文章. 话说看图看核心 ...

  10. AIR780E引脚复用笔记

    1.应用场景:   使用AIR780E模块驱动TM1637数码管驱动芯片,原有方案是AIR724UG+TM1637.为了降低成本,按照官方方案进行代码迁移.   伴随着代码迁移,硬件引脚也需要做相应调 ...