0%

Moore机编码风格

image-20200823220700155

考虑以上状态机。

我们使用Chisel中的Enum来表示状态,使用switch来描述状态转移。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import chisel3._
import chisel3.util._

class SimpleFsm extends Module {
val io = IO(new Bundle {
val badEvent = Input(Bool())
val clear = Input(Bool())
val ringBell = Output(Bool())
})

val green :: orange :: red :: Nil = Enum(3) // Enum的参数是状态的数量
val stateReg = RegInit(green) // 使用状态去初始化Reg

// 状态转移逻辑
switch(stateReg) {
is(green) {
when(io.badEvent) {
stateReg := orange
}
}
is(orange) {
when(io.badEvent) {
stateReg := red
}
}
is(red) {
when(io.clear) {
stateReg := green
}
}
}

// 输出逻辑
io.ringBell := stateReg === red
}

需要注意到,和Verilog不同的是,在实现状态机时,我们并没有在代码中引入next_state这个信号,也就是说,不同于我们在Verilog中描述的三段式/二段式状态机。

其原因大抵是因为,Verilog不允许在同一个过程块中,对组合逻辑和时序逻辑进行同时的赋值更新,而Chisel是允许的。

在后面的Mealy机编码中,我们可以看到这样实现的好处。

Mealy机编码风格

一个最简单的上升沿检测机制,可以使用一行Chisel代码实现:

1
val risingEdge = din & !RegNext(din)

其综合出的电路如图所示:

image-20200823223351935

对于一个上升沿检测,如果我们使用状态机进行表示,其状态转移图如图所示:

image-20200823223610841

由于是Mealy状态机,所以状态机的输出也和输入有关。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import chisel3._
import chisel3.util._

class RisingFsm extends Module {
val io = IO(new Bundle{
val din = Input(Bool())
val risingEdge = Output(Bool())
})

val zero :: one :: Nil = Enum(2)
val stateReg = RegInit(zero)

io.risingEdge := false.B // 组合逻辑输出必须有默认值

switch (stateReg) {
is(zero) {
when(io.din) {
stateReg := one
io.risingEdge := true.B
}
}
is(one) {
when(!io.din) {
stateReg := zero
io.risingEdge := false.B
}
}
}
}

在上述代码中,我们可以看到,和Verilog编码风格完全不同,时序逻辑和组合逻辑在同一个Block(块)中进行了赋值,组合逻辑将立即发生跳变,而时序逻辑将在下一个时钟上升沿更新,刚刚从Verilog转过来的设计者可能不太适应这种风格。

或许我们应该转变思路,应该将 stateReg := one这样的代码看作是次态转移逻辑,而不是对现态寄存器的更新,此时:

猜测这种写法的设计哲学是,将次态转移和输出逻辑写在了同一个块中,方便维护。

Part A: 组合逻辑

组合逻辑描述

在Chisel中,组合逻辑电路最简单的表示方法是将组合逻辑赋值给一个val,并可以通过这个val引用。

1
val e = ( a & b ) | c

以上表达式右侧的逻辑被命名e,对于val的重新赋值是不允许的。

Chisel当然也支持使用条件语句生成的组合逻辑。这样的电路以Wire定义(在Verilog中,我们以Reg定义,在[email protected](*)块中对其赋值)。此后,我们使用条件操作,例如when,来描述这个电路的行为。

1
2
3
4
5
val w = Wire(UInt())
w := 0.U
when(cond) {
w := 3.U
}

when语句还可以后接otherwise语句。

1
2
3
4
5
when (cond) {
w := 1.U
} otherwise {
w := 2.U
}

还可以插入elsewhen,注意代码中的”.”是必要的。

1
2
3
4
5
6
7
when (cond) {
w := 1.U
} .elsewhen(cond2) {
w := 2.U
} otherwise {
w := 3.U
}

如果cond仅仅依赖于一个信号,建议还是使用switch语句。


对于复杂的逻辑,我们应该对Wire赋予初值,以避免锁存器。

1
2
3
4
val w = WireDefault(0.U)
when (cond) {
w := 3.U
}

需要注意的是,Scala中的if,else是控制流代码,是用于控制生成硬件的条件的,并不能用来描述组合逻辑,也并不会生成硬件。

Example: Decoder

Decoder一般可以使用switch语句来进行描述。

1
2
3
4
5
6
7
8
9
10
import chisel3.util._

result := 0.U

switch(sel) {
is(0.U) { result := 1.U }
is(1.U) { result := 2.U }
is(2.U) { result := 4.U }
is(3.U) { result := 8.U }
}

注意:就算在switch里面列举了所有的可能,我们仍然需要给某个信号赋予初值,为了防止生成锁存器的可能,Chisel严禁使用不完备的赋值。

Part B: 时序逻辑

时序逻辑描述:寄存器

image-20200823174343426

在Chisel中,上图寄存器可以被这样描述:

1
val q = RegNext(d)

我们可以注意到,这个寄存器并没有显式地声明时钟、声明复位,端口被默认的连接到了全局时钟上,注意:Chisel不支持异步复位。

RegNext会自动根据输入信号推断寄存器的宽度。

还可以使用以下的形式,声明一个寄存器,并赋值:

1
2
val q = Reg(UInt(4.W))
q := data_in
  • 由于在Chisel中,并不存在阻塞赋值和非阻塞赋值,也不存在always块,所以难以某个赋值语句是在对寄存器赋值还是组合逻辑赋值。

    • 推荐的命名规范是,寄存器信号以Reg结尾!
    • 并且,在Scala推荐的命名规范中,建议采用驼峰命名法
  • 在声明寄存器时,还可以对寄存器指定初始值。

1
2
val valReg = RegInit(0.U(4.W))
valReg := inVal
  • 声明一个带使能的寄存器:
1
2
3
4
val enableReg = RegInit(0.U(4.W))
when(enable) {
enableReg := inVal
}
  • 寄存器甚至可以在表达式中声明,甚至作为表达式的一个部分进行使用,但此时,这是一个匿名的寄存器:
1
val risingEdge = din & !RegNext(din)

此处有一个思考,之前使用val来定义某个逻辑,不管是寄存器也好,还是组合逻辑也好,本质上是对某个硬件节点进行命名操作。此处在表达式中使用RegNext,相当于创建了一个新的寄存器,并匿名地引用它的值,其用法和组合逻辑有点类似。

Example: Counter

一个最简单的计数器:

1
2
3
4
5
val cntReg = RegInit(0.U(8.W))
cntReg := cntReg + 1.U
when(cntReg === N) {
cntReg := 0.U
}

或者,我们可以使用一个多选器来决定计数器的输入:

1
2
val cntReg = RegInit(0.U(8.W))
cntReg := Mux(cntReg === N, 0.U, cntReg + 1.U)

再高层次地,我们使用一个计数器生成器:

1
2
3
4
5
6
def genCounter(n: Int) = {
val cntReg = RegInit(0.U(8.W))
cntReg := Mux(cntReg === n.U, 0.U, cntReg + 1.U)
cntReg
}
val cnt99 = genCounter(99)

注意函数的最后一行,代表返回一个创建好的硬件节点。

Counter使用实例

image-20200823183445450

1
2
3
4
5
6
7
8
9
// tick是一个三分频时钟,其占空比不为0.5
val tickCounterReg = RegInit(0.U(4.W))
val tick = tickCounterReg === (N-1).U

tickCounterReg := Mux(tick, 0, tickCounterReg + 1.U)
val lowFreqCntReg = RegInit(0.U(4.W))
when(tick) {
lowFreqCntReg := lowFreqCntReg + 1.U
}

其中,tick是一个占空比不为50%的时钟,控制一个更慢的计数器进行更新操作。

更优化的计数器

我们转变思路,将计数器转变为递减计数器。从N-1数到0,转变为N-2数到-1,计满的情况,仅需要看计数器最高位即可。

Example: PWM

image-20200823191133656

PWM调制,时钟占空比随着时间的变化而变化。

以下的代码可以生成占空比为0.7的PWM波形:

1
2
3
4
5
6
7
def pwm(nrCycles: Int, din: UInt) = {
val cntReg = RegInit(0.U(unsignedBitLength(nrCycles-1).W)) // 自动计算宽度
cntReg := Mux(cntReg === (nrCycles-1).U, 0.U, cntReg + 1.U) // 普通的循环计数器
din > cntReg // 当din > cntReg的时候为高,din即为输出从高到低的交界点
}

val pwm_out = (10, 3.U)

上述代码即为一个可复用的、轻量的代码。它接收两个参数,一个是时钟周期的数量(nrCycles),另一个是给出了脉冲宽度(din)。

注意:函数的最后一行是返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val FREQ = 100000000 // a 100 MHz clock input
val MAX = FREQ /1000 // 1 kHz
val modulationReg = RegInit (0.U(32.W))
val upReg = RegInit(true.B)
when ( modulationReg < FREQ.U && upReg) {
modulationReg := modulationReg + 1.U
} .elsewhen ( modulationReg === FREQ.U && upReg) {
upReg := false.B // 计数加模式
} .elsewhen ( modulationReg > 0.U && !upReg) {
modulationReg := modulationReg - 1.U // 计数减模式
} . otherwise { // 0
upReg := true.B
}
// divide modReg by 1024 (about the 1 kHz)
val sig = pwm(MAX , modulationReg >> 10)

Example: 移位寄存器

1
2
3
val shiftReg = Reg(UInt(4.W))
shiftReg := Cat(shiftReg(2,0), din)
val dout = shiftReg(3)

Cat用于串接输入输出,对于Reg和Wire的位选,使用括号,也就是Scala中的Apply()方法。

存储器

存储器可以用寄存器阵列进行构建,也可以使用SRAM。在ASIC设计中,使用memory compiler进行设计;在FPGA设计中,片上拥有block RAM资源。

Chisel提供了存储器构建器SyncReadMem,对于同时读写一个地址,Chisel将其定义为未定义的行为,此时,我们可以手动进行旁路。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MemoryWithForwarding() extends Module {
val io = IO(new Bundle{
val rdAddr = Input(UInut(10.W))
val rdData = Input(UInt(8.W))
val wrEna = Input(Bool())
val wrData = Input(UInt(8.W))
val wrAddr = Input(UInt(10.W))
})
val mem = SyncReadMem(1024,UInt(8.W))

val wrDataReg = RegNext(io.wrData)
val doForwardingReg = RegNext(io.wAddr === io.rdAddr && io.wrEna)

val memData = mem.read(io.rdAddr)

when(io.wrEna) {
mem.write(io.wrAddr, io.wrData)
}

io.rdData := Mux(doForwardingReg, wrDataReg, memData)
}

Chisel也提供了Mem,异步读,同步写,类似于FPGA中的distributed RAM

Chisel模块

在Chisel中,每一个硬件模块继承Module类,并且拥有一个名为io的域。IO被Bundle所包裹,端口的方向通过调用Input()或者Output()来声明。

以下是声明两个组件的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CompA extends Module {
val io = IO(new Bundle {
val a = Input(UInt(8.W))
val b = Input(UInt(8.W))
val x = Output(UInt(8.W))
val y = Output(UInt(8.W))
})
}

class CompB extends Module {
val io = IO(new Bundle {
val in1 = Input(UInt(8.W))
val in2 = Input(UInt(8.W))
val out = Output(UInt(8.W))
})
}

以下是在顶层模块中实例化两个部件,并进行连线的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class CompC extends Module {
val io = IO(new Bundle {
val in_a = Input(UInt(8.W))
val in_b = Input(UInt(8.W))
val in_c = Input(UInt(8.W))
val out_x = Output(UInt(8.W))
val out_y = Output(UInt(8.W))
})
val compA = Module(new CompA())
val compB = Module(new CompB())

compA.io.a := io.in_a
compA.io.b := io.in_b
io.out_x := compA.io.x

compB.io.in1 := compA.io.y
compB.io.in2 := io.in_c
io.out_y := compB.io.out
}

组件通过new关键字来创建,并且需要被包裹在一个对于Module的调用中。

对于组件的引用存储在一个本地变量,例如,compA中,可以通过io域来访问组件的IO端口。

实例:ALU

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import chisel3.util._
class Alu extends Module {
val io = IO(new Bundle{
val a = Input(UInt(16.W))
val b = Input(UInt(16.W))
val fn = Input(UInt(2.W))
val y = Output(UInt(16.W))
})
io.y := 0.U // 需要赋初值
switch(io.fn) {
is(0.U) { io.y := io.a + io.b }
// .................
}
}

这里使用了switch/is表达式来表示case。

批量端口连接

我们可以使用<>运算符进行批量端口连接。Chisel会自动对那些同名的、方向相反的端口进行连接,如果没有同名端口,那么则不会被连接。

例如,流水线处理器中的几个阶段:

1
2
3
4
5
6
7
class Fetch extends Module {
val io = IO(new Bundle {
val instr = Output(UInt(32.W))
val pc = Output(UInt(32.W))
})
// Fetch
}
1
2
3
4
5
6
7
class Decode extends Module {
val io = IO(new Bundle {
val instr = Input(UInt(32.W))
val pc = Input(UInt(32.W))
val aluOp = Ouput(UInt(32.W))
})
}

在顶层模块对他们进行连接的时候,可以:

1
2
3
4
5
6
class cpu extends Module {
val io = IO( ..... )
val ifu = Module(new Fetch())
val dec = Module(new Decode())
ifu.io <> dec.io // 完成同名端口的连接
}

在实际编写代码的时候,我们为了清晰起见,可以使用src_dst的后缀来编码信号。

使用函数生成轻量化的模块

某些硬件模块 ,除了参数变动,或是连线变动之外,不会发生其他的改变,这时候我们可以使用函数返回相应的硬件模块。

比Verilog的参数化更强大的是,函数可以以实际的硬件信号为输入,这样,就免去了声明模块-实例化模块-连线的过程。

例如,我们定义一个加法器。

1
2
3
def adder ( x: UInt, y: UInt ) = {
x + y
}

我们可以非常轻松地使用adder函数创建,并连线两个加法器。

1
2
val x = adder(a,b) 
val y = adder(c,d)

应该可以注意到,这个函数是一个硬件生成器。在调用函数的过程中,并没有进行加法的运算,而是只是在函数里面创建了一个新的硬件节点,并对信号进行了连接。这个例子带来的收益并不明显,因为加法在Chisel中已经是一个重载过的运算符了。

函数中同样可以使用寄存器,函数被多次调用,会产生多个硬件的寄存器实例。

例如,我们声明一个delay函数,将某个信号延时一拍。

1
def delay(x: UInt) = RegNext(x)

我们可以嵌套调用delay函数,生成一个将信号延迟两拍的器件。

1
val delay_2cyc = delay(delay(delay_in))

函数可以作为Module的一个部分被声明,然而,如果是在不同的Module中被使用的函数,最好被放在一个专门的object中。

本节主要记录如何编译Chisel代码,生成Verilog和测试我们的Chisel设计。由于Chisel使用Scala实现,构建Scala的常用工具是sbt。在编译和测试的过程中,sbt下载对应版本的Scala和Chisel库。

使用sbt构建项目

  • 在构建项目时,会通过build.sbt中指出的库,从Maven仓库中下载相应版本的Chisel。
  • 如果在build.sbt中使用latest.release,则每次都会下载Chisel的最新版本
  • 推荐在build.sbt中指明Chisel版本,以达到离线使用的目的

源文件组织

image-20200823131332598

推荐的文件组织方式如上图所示。

Package将Chisel代码划分成命名空间,Package还可以包括不同的子Pacakge。

Target文件夹包括了那些类的文件和其他生成好的文件。

generated文件夹包含了生成的Verilog代码。

以下是一个Package的实例:

1
2
3
4
5
6
pacakge mypack
import chisel3._

class Abc extens Module {
val io = IO(new Bundle{})
}

注意到我们导入了Chisel包。

如果要在其他的环境(不同的命名空间)中使用Abc模块,需要导入mypack包,下划线代表通配符,表示导入mypack包中所有的类。

1
import mypack._

也可以导入一部分类,此时需要指明包名和类名。

1
2
3
4
class AbcUser2 extends Module {
val io = IO(new Bundle{})
val abc = new mypack.Abc()
}
1
2
3
4
5
import mypack.Abc
class AbcUser3 exteds Module {
val io = IO(new Bundle{})
val abc = new Abc()
}

运行sbt

Chisel项目可以用sbt命令编译并运行:

1
sbt run

这个命令将搜索Chisel源码树中的满足:拥有包含main方法的object的类 / 继承了App类的类。如果有多个满足条件,将会列出所有供选择。

当然也可以进行显式的选择。

1
sbt "runMain mypacket.MyObject"

Chisel测试程序包含main方法,但被放在了test文件夹中,并不会被sbt扫描到(这是Java/Scala的一个约定,认为test文件夹中仅包含单元测试,并没有main方法)。为了在tester文件夹中执行,需要使用如下的命令:

1
sbt "test:runMain mypacket.MyTester"

工具流

image-20200823131357855

电路的RTL文件是Hello.scala,Scala编译器将这个文件和Chisel库、Scala库一起编译,生成Java类Hello.class,使用JVM进行执行,执行后生成RTL的中间表示(FIRRTL)。

Treadle是一个FIRRTL的解释器,可以用来仿真电路,可以生成波形文件。

Verilog Emitter可以生成Verilog代码。

使用Chisel进行测试

DUT: Design Under Test, 被测模块

PeekPokeTester

Chisel可以使用Scala的很多特性来编写TB,例如,可以使用软件来编写硬件的功能,并且在仿真时和真正的硬件行为进行比较。例如,在[Martin Schoeberl. Lipsi: Probably the smallest processor in the world. In Architecture of Computing Systems – ARCS 2018, pages 18–30. Springer International Publishing, 2018.]中,实现一个处理器并进行测试。

需要导入的包有:

1
2
import chisel3._
import chisel3.iotesters._

对电路进行测试,需要包含三个元素:

  • 用于测试的单元
  • 测试逻辑
  • 测试对象,包含开始测试的main方法

DUT(被测对象)

1
2
3
4
5
6
7
8
9
class DeviceUnderTest extends Module {
val io = IO(new Bundle {
val a = Input(UInt(2.W))
val b = Input(UInt(2.W))
val out = Output(UInt(2.W))
} )

io.out := io.a & io.b
}

测试逻辑

测试对象需要从PeekPokeTester中继承,并且以DUT作为参数传入。

1
2
3
4
5
6
7
8
9
10
11
class TesterSimple(dut: DeviceUnderTest ) extends
PeekPokeTester(dut) {
poke(dut.io.a, 0.U)
poke(dut.io.b, 1.U)
step (1)
println("Result is: " + peek(dut.io.out).toString)
poke(dut.io.a, 3.U)
poke(dut.io.b, 2.U)
step (1)
println("Result is: " + peek(dut.io.out).toString)
}

PeekPokeTester可以用poke()设置输入,并使用peek()获取输出。使用step(1)来前进一个时钟周期。

测试对象(object)

1
2
3
4
5
object TesterSimple extends App {
chisel3. iotesters .Driver (() => new DeviceUnderTest ()) { c =>
new TesterSimple (c)
}
}

也可以使用expect断言,让程序自动检查测试是否通过。

1
2
3
4
5
6
7
8
9
10
class Tester(dut: DeviceUnderTest ) extends PeekPokeTester(dut) {
poke(dut.io.a, 3.U)
poke(dut.io.b, 1.U)
step (1)
expect(dut.io.out , 1)
poke(dut.io.a, 2.U)
poke(dut.io.b, 0.U)
step (1)
expect(dut.io.out , 0)
}

ScalaTest

ScalaTest是一个用于测试Scala的工具,我们也可以用来运行Chisel测试。

1
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.5" % "test"

使用

1
sbt test

就可以运行在src/test/scala下面的测试程序。

一个简单的测试程序如下:

1
2
3
4
5
6
7
8
import org. scalatest ._
class ExampleSpec extends FlatSpec with Matchers {
"Integers" should "add" in {
val i = 2
val j = 3
i + j should be (5)
}
}

事实上,我们可以将Chisel的测试包裹到Scala中去。

1
2
3
4
5
6
7
class SimpleSpec extends FlatSpec with Matchers {
"Tester" should "pass" in {
chisel3. iotesters .Driver (() => new DeviceUnderTest ()) { c =>
new Tester(c)
} should be (true)
}
}

这样做的好处是,可以使用

1
sbt "testOnly SimpleSpec"

来运行单个测试。

波形图

要生成波形图,应该在进行测试时添加一定的参数。

1
2
3
4
5
6
7
8
class WaveformSpec extends FlatSpec with Matchers {
"Waveform" should "pass" in {
Driver.execute(Array("--generate -vcd-output", "on"), () =>
new DeviceUnderTest ()) { c =>
new WaveformTester (c)
} should be (true)
}
}

生成的vcd波形图文件可以使用GTKWake或者Modelsim打开。

printf调试

在Chisel中写printf语句,在时钟的上升沿会触发printf语句。

1
2
3
4
5
6
7
8
9
class DeviceUnderTestPrintf extends Module {
val io = IO(new Bundle {
val a = Input(UInt (2.W))
val b = Input(UInt (2.W))
val out = Output(UInt (2.W))
})
io.out := io.a & io.b
printf("dut: %d %d %d\n", io.a, io.b, io.out)
}

本文主要记录使用Hexo+Next主题搭建博客,并部署在Github Pages,使之能够通过个人域名访问的全过程。

准备

环境配置

首先需要安装node环境,安装完毕后,在命令行中输入:

1
npm install hexo-cli -g

安装完毕后,进入某个目录,在命令行中输入

1
2
3
hexo init blog
cd blog
npm install

然后安装Next主题:

1
npm install hexo-theme-next

以上命令会将next主题依赖添加到package.json文件中,方便后续在Github CI上部署。

修改Hexo配置文件

此后,修改_config.yml文件,找到theme这一行:

1
theme: next

根据hexo文档中的指导,修改其他的内容。

https://hexo.io/zh-cn/docs/configuration

修改Next配置文件

将配置文件复制到根目录:

1
cp node_modules/hexo-theme-next/_config.yml _config.next.yml

具体设置参考Next文档。

https://theme-next.js.org/docs/theme-settings/

根据文档,添加about, tags, categories页面。

部署到Github

建立Github仓库

在Github中建立一个公开仓库,命名为<name>.github.io

在Hexo配置文件中,修改最后三行:

1
2
3
4
deploy:
type: git
repository: [email protected]:Bohan-hu/Bohan-hu.github.io.git
branch: master

建立源代码仓库

在本地刚刚建立的Blog文件夹中,新建.github\workflows\deploy.yml,用于CI自动部署。

image-20200823133939523

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
name: CI

on:
push:
branches:
- master

env:
GIT_USER: Bohan-Hu
GIT_EMAIL: [email protected]
DEPLOY_REPO: Bohan-hu/Bohan-hu.github.io
DEPLOY_BRANCH: master

jobs:
build:
name: Build on node ${{ matrix.node_version }} and ${{ matrix.os }}
runs-on: ubuntu-latest
strategy:
matrix:
os: [ubuntu-latest]
node_version: [12.x]

steps:
- name: Checkout
uses: actions/[email protected]

- name: Checkout deploy repo
uses: actions/[email protected]
with:
repository: ${{ env.DEPLOY_REPO }}
ref: ${{ env.DEPLOY_BRANCH }}
path: .deploy_git

- name: Use Node.js ${{ matrix.node_version }}
uses: actions/[email protected]
with:
node-version: ${{ matrix.node_version }}

- name: Configuration environment
env:
HEXO_DEPLOY_PRI: ${{secrets.HEXO_DEPLOY_PRI}}
run: |
sudo timedatectl set-timezone "Asia/Shanghai"
mkdir -p ~/.ssh/
echo "$HEXO_DEPLOY_PRI" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan github.com >> ~/.ssh/known_hosts
git config --global user.name $GIT_USER
git config --global user.email $GIT_EMAIL

- name: Install dependencies
run: |
npm install

- name: Deploy hexo
run: |
npm run deploy

配置仓库权限

此后,在本地生成一对公私密钥。

1
ssh-keygen -t rsa -f

在Github中,对于网站源代码仓库,将private key在源文件仓库中加入到HEXO_DEPLOY_PRI变量中,将public key放入网站的文件仓库中的Deploy keys中。

在Github新建一个私有仓库,将网站源文件(即Hexo刚刚建立的文件夹)推上去,观察CI自动部署的情况。

使用个人域名访问

域名的服务商此处选择Godaddy,并使用Cloudflare提供的解析服务。

设置Cloudflare

进入Cloudflare,添加刚刚注册的域名,并进入DNS选项卡,添加A记录。

image-20200823134133941

设置完毕后,需要一定时间生效,此时,进入SSL/TTS-边缘证书-始终使用HTTPS:

image-20200823134157565

设置Godaddy

在Godaddy注册域名后,在域名解析的选项中,设置为Cloudflare的域名服务器。

image-20200823134210525

添加CNAME

在博客文件夹的source子目录中,新建CNAME文件,用大写写入自己绑定到Github的域名。

image-20200823134225476

至此,可以通过域名访问自己的博客主页了。

设置图床

由于Hexo添加图片较为麻烦,建议使用图床,国内使用Gitee作为图床是比较好的选择。

8.23更新:由于Gitee对于微信浏览器的UA做了限制,所以现在采用Github+JsDelivr的方案。

安装Picgo并建立Github仓库

在Picgo官网下载安装Picgo后,在Github中建立一个新的公开仓库,同时需要使用Readme初始化,以新建Master分支。

配置插件

注意链接格式,前缀为https://cdn.jsdelivr.net/gh/用户名/仓库名

image-20200823134358958

配置Typora

image-20200823134508984

如果出现Fetch Error,更改监听端口为36677.

image-20200823134519328