LOADING

加载过慢请开启缓存 浏览器默认开启

染玥的博客

condering the world

2024/9/6

如何减少或者改写代码中的if

方法一:

映射表(Map/Dictionary):使用映射表来替代if语句。将条件和相应的操作映射到一个关联容器中,然后根据条件查找并执行相应的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <map>
#include <functional>

void Action1() { std::cout << "Action 1" << std::endl; }
void Action2() { std::cout << "Action 2" << std::endl; }
void Action3() { std::cout << "Action 3" << std::endl; }

int main() {
std::map<int, std::function<void()>> actionMap;
actionMap[1] = Action1;
actionMap[2] = Action2;
actionMap[3] = Action3;

int condition = 2;
if (actionMap.find(condition) != actionMap.end()) {
actionMap[condition]();
} else {
std::cout << "Condition not found" << std::endl;
}

return 0;
}

方法二:

表驱动编程(Table-Driven Programming):将条件和操作的关联数据结构化存储,然后使用查找表来执行操作,而不是使用if语句。

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
#include <iostream>

// 定义一个结构体,用于存储条件和相应的操作
struct ConditionAction {
int condition;
void (*action)();
};

// 定义操作函数
void Action1() { std::cout << "Action 1" << std::endl; }
void Action2() { std::cout << "Action 2" << std::endl; }
void Action3() { std::cout << "Action 3" << std::endl; }

int main() {
// 创建一个表驱动数组,存储条件和相应的操作
ConditionAction conditionActions[] = {
{1, Action1},
{2, Action2},
{3, Action3}
};

int condition = 2; // 设置条件
// 遍历数组,找到匹配的条件并执行相应的操作
for (const auto& ca : conditionActions) {
if (ca.condition == condition) {
ca.action();
return 0; // 找到匹配条件后可以退出循环
}
}

std::cout << "Condition not found" << std::endl;
return 0;
}

方法三:

什么是有限状态机?

有限状态机(finite state machine,简称 FSM),有时也被称为 finite state automation,有时就简单地叫 state machine。它是一种常用的解决状态扭转问题的方法,通过定义状态以及状态之间的转移规则来控制状态的流转。

要理解有限状态机,首先我们需要理解整个模型。我们以灯的开关为例,先看图:

image-20231106131539031

如上图所示,可以提取出几个关键点:

1.状态

2.动作

3.状态过渡

4.转换事件

5.FSM

一般状态都会对应一个动作,所以我们把状态和动作放在状态里面,然后状态过渡和过渡事件放在状态过渡里面,最后由状态机驱动。于是一个状态机模型就有了,状态、状态过渡、状态机。

其中我们最容易忽略的一点就是状态过渡,这是个最不容易想到和抽象出来的一个对象,为什么要把状态过渡抽象出来呢?因为状态过渡就是每个状态之间相互转换的条件,这个抽象出来后,以后就可以解耦每个状态之间的转换条件,少些许多分支判断且更利用维护

网上下过单都知道,一个订单从创建开始要经历好几个状态,中间也有不同的操作可以进行,下面是一个比较典型的流程设计,经过一定简化,并以“状态”的主视角来描绘:

image-20231106131700846

图中圆边的矩形代表状态,最上面一排是“正常”的状态和流程;第二排的菱形则表示一些“逆向”子流程,通常是由用户或客户发起的特殊操作,这些操作会带来其他一些订单状态,为了简单起见没有在这里展开。

流程说明:

1.当买家点击下单时订单生成,处于“已创建”状态;这个状态下其他可选操作包括“修改”、“取消”等,分别会去到订单修改和订单取消子流程(略);

2.这个状态下的正常操作是“支付”,如果输入“支付成功”会进入下一个状态“已支付”,“支付失败”或者没有任何操作则停在本状态;

支付成功后进入处于“已支付”状态;这个状态下需要等待商家发货,商家输入“已发货”会进入下一个状态“配送中”;

3.这个状态下不能修改订单了,但仍然可以取消订单;商家发货后进入“配送中”状态;

当配送到货,买家签收成功输入则进入下一个状态“已签收”;如果配送失败(买家不在家之类的情况)则留在“配送中”状态(另外择时重新送货);

这个状态下已不能修改和取消订单,但是可以发起退货申请,进入退货子流程(略);

4.买家签收后进入“已签收”状态;

5.买家满意,确认订单完成则进入最后状态“已完成”,订单生命周期结束;

否则买家可以发起退货进入退货子流程(略)。

从这里我们可以看到,实际业务系统中状态和转换的规则相当复杂(我们这还是大大简化的版本),每个状态下允许的操作和可能转换的下一个状态都是严格受控的,现在我们思考一下,我们可以如何用程序来实现这样的流程呢?

利用有限状态机编写易于维护的代码

回忆我们之前提到的,流程和行为控制的关键是管理:

在某个状态下什么能做什么不能做;

做了什么会变成另外的什么状态。

最简单直接的办法就是书写一堆 if…else 的判断规则,大致会是这个样子:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package main

import "errors"

type OrderState uint8

const (
Created OrderState = 1 << iota
Paid
Delivering
Received
Done
Cancelling
Returning
Closed
)

type order struct {
state OrderState
}

func NewOrder() *order {
return &order{
state: Created,
}
}

func (o *order) CanPay() bool {
return o.state == Created
}

func (o *order) CanDeliver() bool {
return o.state == Paid
}

func (o *order) CanCancel() bool {
return o.state == Created || o.state == Paid
}

func (o *order) CanReceive() bool {
return o.state == Delivering
}

func (o *order) PaymentService() bool {
//调用远程接口完成实际支付

return false
}

// 然后是关键操作的实现,比如支付
func (o *order) Pay() (bool, error) {
if o.CanPay() {
if ok := o.PaymentService(); ok {
o.state = Paid
return true, nil
}

} else {
//抛出错误
return false, errors.New("Pay Error")
}

return false, nil
}

// 然后是关键操作的实现,比如支付
func (o *order) Cancel() (bool, error) {
if o.CanCancel() {
// 取消订单
o.state = Cancelling

// 取消订单,申请审批和清理数据,如果顺利成功再关闭订单
o.state = Closed
} else {
// 抛出错误
return false, errors.New("Cancel Error")
}

return false, nil
}

这样的代码非常冗长和重复,难以维护且难以修改,设想一下,假设在上面的基础上再增加一个状态,要连带修改不确定几处地方,做完这样的修改还需要相应修改所有的测试用例,累就不说了,关键是容易出错。

有限状态机实际上是这些“八股”的通用实现,然后提供一个非常简洁的接口供我们使用。有兴趣的话可以自己尝试用 Golang 写一个 FSM 的实现出来,只做最基本功能的话也不是很难,但我们实际上没必要自己写, Golang也有不少FSM的第三方实现,比如 github.com/looplab/fsm 这个库,我们就可以用它来展示一下上面的流程如何用 FSM 来实现:

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
package main

import (
"context"
"fmt"
"github.com/looplab/fsm"
)

func main() {
// 创建有限状态机对象
order := fsm.NewFSM(
"created", // 初始状态
fsm.Events{
{Name: "pay", Src: []string{"created"}, Dst: "paid"},
{Name: "deliver", Src: []string{"paid"}, Dst: "delivering"},
{Name: "receive", Src: []string{"delivering"}, Dst: "received"},
{Name: "confirm", Src: []string{"received"}, Dst: "done"},
},
fsm.Callbacks{
"before_pay": func(e *fsm.Event) {
fmt.Println("支付服务申请中……")
// 可以在此处执行相关操作
},
"paid": func(e *fsm.Event) {
fmt.Println("支付成功")
},
"after_deliver": func(e *fsm.Event) {
fmt.Println("已通知用户:商品配送中")
},
},
)

// 获取当前状态
fmt.Println(order.Current())

// 触发 "pay" 事件
err := order.Event(context.Background(), "pay")
if err != nil {
fmt.Println(err)
}

// 获取当前状态
fmt.Println(order.Current())

// 触发 "deliver" 事件
err = order.Event(context.Background(), "deliver")
if err != nil {
fmt.Println(err)
}

// 获取当前状态
fmt.Println(order.Current())

// 可以继续触发其他事件
}

  1. 创建有限状态机对象:

    使用 fsm.NewFSM 创建了一个有限状态机对象,指定了初始状态、事件以及状态转换关系。

  2. 定义事件和状态转换关系:

    fsm.Events 部分,定义了不同事件(例如 “pay”、”deliver”)以及它们可以触发的状态转换关系。

  3. 定义事件触发的回调函数:

    使用 fsm.Callbacks 部分定义了不同事件触发时执行的回调函数。这些回调可以在不同事件的不同阶段执行,以处理相关逻辑。

  4. 使用有限状态机执行事件:

    使用 order.Event 方法来触发状态机中定义的事件。如果事件执行成功,err 将为 nil;否则,它将包含一个错误信息。

  5. 获取当前状态:

    使用 order.Current 方法来获取当前状态。

deliver 事件

在有限状态机的定义中,您可以看到以下部分:

1
2
3
4
5
fsm.Events{
// ...
{Name: "deliver", Src: []string{"paid"}, Dst: "delivering"},
// ...
}

这部分定义了一个名为 “deliver” 的事件,它的源状态(Src)是 “paid”,目标状态(Dst)是 “delivering”。这意味着在当前状态为 “paid” 时,触发 “deliver” 事件将导致状态转换到 “delivering”。

after_deliver 回调

在回调部分,定义了一个名为 “after_deliver” 的回调函数,如下:

1
2
3
4
5
6
7
8
fsm.Callbacks{
// ...
"after_deliver": func(e *fsm.Event) {
fmt.Println("已通知用户:商品配送中")
},
// ...
}

这个回调函数会在 “deliver” 事件触发之后执行,因为它的名称是 “after_deliver”。所以,当 “deliver” 事件导致状态从 “paid” 转换到 “delivering” 时,"after_deliver" 回调函数将被执行,输出 “已通知用户:商品配送中”。

总结一下,事件和回调之间的关联是通过事件的名称和回调的名称建立的。在状态机中定义事件和回调后,事件触发时,与之关联的回调函数将被执行,以执行相应的操作。在这个示例中,”deliver” 事件触发后,”after_deliver” 回调函数会输出一条消息。

github.com/looplab/fsm 包中,回调函数的命名是有含义的。具体来说,回调函数的名称中包含 before_after_ 前缀,以表示回调函数在状态机事件的执行顺序中的时机。

  • before_ 前缀:回调函数带有 before_ 前缀的名称会在执行与之关联的事件之前被调用。这些回调函数可以用于在事件执行前执行一些前期准备工作,或者可以用于在事件执行之前进行一些验证或条件检查。
  • after_ 前缀:回调函数带有 after_ 前缀的名称会在执行与之关联的事件之后被调用。这些回调函数可以用于在事件执行后进行一些清理工作或其他操作,也可以用于处理事件执行后的后续逻辑。

这种命名约定使得在状态机的事件处理过程中,可以很清晰地识别哪些回调函数是在事件执行前调用的,哪些是在事件执行后调用的。这有助于更好地控制事件处理的流程,同时使代码更易于理解和维护。

例如,在代码示例中,定义的回调函数中有 “before_pay” 和 “after_deliver” 回调函数。 “before_pay” 会在 “pay” 事件执行前被调用,而 “after_deliver” 会在 “deliver” 事件执行后被调用,从而帮助控制事件的前后逻辑。

github.com/looplab/fsm 包中,状态转换后的新状态(目标状态)可以与回调函数同名,以使状态机在进入新状态时执行特定的回调函数。这是一种约定,用于执行与状态同名的回调函数。

在您的示例中,有一个状态转换将状态从 “created” 转换为 “paid”,并且在 “paid” 状态下,有一个同名的回调函数 “paid”。这意味着当状态机执行从 “created” 到 “paid” 的状态转换时,状态机会自动查找并执行名为 “paid” 的回调函数。

具体来说,在状态机执行以下代码时:

1
err := order.Event(context.Background(), "pay")

状态机会检查当前状态是否为 “created” 并且事件 “pay” 是否允许从 “created” 转换到 “paid”,如果条件满足,状态机会执行状态转换,并在状态变为 “paid” 后执行同名的回调函数 “paid”。

这种方式可以用于在状态转换的过程中执行特定的逻辑。同名的回调函数的参数通常是 *fsm.Event 对象,它可以提供关于状态机事件的信息以及一些控制功能。例如,在 “paid” 回调中,您可以执行与订单支付相关的操作

关于cpp:GitHub:https://github.com/boost-experimental/sml

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
58
#include <iostream>
#include <sml.hpp>

namespace sml = boost::sml;

// Define states
struct created {};
struct paid {};
struct delivering {};
struct received {};
struct done {};

// Define events
struct pay {};
struct deliver {};
struct receive {};
struct confirm {};

/*
*state<created> + event<pay> = state<paid>:这一行表示了一个状态转换规则。它的含义是,在状态为 created 的情况下,如果触发了 pay 事件,那么状态将从 created 转换到 paid。
state<paid> + event<deliver> = state<delivering>:这行表示了另一个状态转换规则。在状态为 paid 的情况下,如果触发了 deliver 事件,状态将从 paid 转换到 delivering。
*/

// Define a state machine
struct order_process {
auto operator()() const {
using namespace sml;

return make_transition_table(
*state<created> + event<pay> = state<paid>,
state<paid> + event<deliver> = state<delivering>,
state<delivering> + event<receive> = state<received>,
state<received> + event<confirm> = state<done>
);
}
};

int main() {
sml::sm<order_process> sm;

std::cout << "Current state: " << sm.current_state().c_str() << std::endl;

// Trigger events
sm.process_event(pay{});
std::cout << "Current state: " << sm.current_state().c_str() << std::endl;

sm.process_event(deliver{});
std::cout << "Current state: " << sm.current_state().c_str() << std::endl;

sm.process_event(receive{});
std::cout << "Current state: " << sm.current_state().c_str() << std::endl;

sm.process_event(confirm{});
std::cout << "Current state: " << sm.current_state().c_str() << std::endl;

return 0;
}

  1. 首先,我们定义了一组状态(created, paid, delivering, received, done)和一组事件(pay, deliver, receive, confirm)。
  2. 然后,我们创建一个名为 order_process 的状态机,它使用 SML 的 make_transition_table 来定义状态转换。每个状态转换规则表示了从一个状态到另一个状态的事件触发条件。
  3. main 函数中,我们创建了状态机对象 sm,并使用 process_event 方法触发不同事件。随着事件的触发,状态机会根据定义的状态转换规则自动切换状态。
  4. 最后,我们使用 sm.current_state() 来获取当前状态并打印出来。
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
58
59
#include <iostream>
#include <sml.hpp>

namespace sml = boost::sml;

// Define states
struct created {};
struct paid {};
struct delivering {};
struct received {};
struct done {};

// Define events
struct pay {};
struct deliver {};
struct receive {};
struct confirm {};

// Define a state machine
struct order_process {
auto operator()() const {
using namespace sml;

return make_transition_table(
*state<created> + event<pay> = state<paid>,
state<paid> + event<deliver> = state<delivering>,
state<delivering> + event<receive> = state<received>,
state<received> + event<confirm> = state<done>,

on_entry<delivering> / [](const auto&) {
std::cout << "Entering 'delivering' state" << std::endl;
},

on_exit<delivering> / [] {
std::cout << "Exiting 'delivering' state" << std::endl;
},

on<pay> / [](const auto&) {
std::cout << "Payment callback: Payment received" << std::endl;
},

on<receive> / [](const auto&) {
std::cout << "Receive callback: Order received" << std::endl;
}
);
}
};

int main() {
sml::sm<order_process> sm;

sm.process_event(pay{});
sm.process_event(deliver{});
sm.process_event(receive{});
sm.process_event(confirm{});

return 0;
}

阅读全文

S8-Device

2024/7/12

关于将S8驱动部分逻辑迁移到用户空间的分析

Linux内核开发有一个原则:内核提供机制,用户空间提供策略。也即内核应提供基础设施的访问机制,而业务逻辑应该留在用户空间解决。基于这一思想,内核提供的基础设施往往十分简陋,这对于通常的驱动和基础架构代码已经足够,但不足以支撑复杂的业务逻辑开发。S8驱动部分代码将应用层业务逻辑强行包含进核,给简单的业务逻辑带来了不必要的复杂性,降低了代码的可扩展和可维护性。本文档从功能和实现角度将S8驱动代码和底层资源做划分,找出适合移动到用户空间的部分,也为开发新功能做参考。

(一)从IO资源角度分析:

软件侧总共使用了以下IO资源:内存,中断,GPIO,LED,EERPOM,I2C,DAC,ADC,全局时钟,电源,PWM,USB。

(1)从资源访问的方便性角度分析

  1. 内存可以在内核或者用户空间访问。这部分资源的划分原则就是如果涉及的功能必须在内核实现,比如要实现只有内核驱动支持的复杂协议(I2C等),就将其放在内核空间;而如果涉及的功能比较简单且独立,就将其放到用户空间用mmap实现。
  2. 中断可以在内核通过中断子系统处理或者通过U10在用户空间处理。需要特别指出的是,上板的中断会通过中断聚合映射到SCM板的一条中断线上,因为这条线已经被GPIOController之类的内核基础设施占用,所以这个中断不会通过U10映射到用户空间;对于使用其他中断线的使用场景,可以转为在用户空间通过U10实现。
  3. GPIO的使用分为两部分,一部分是GPIOCONTROLLER驱动,这个作为基础设施必须留在内核,另一部分是对每个GPIO线的申请和使用,这个除非是给迁移后保留在内核的模块使用,其他一律改为在用户空间用libgpiod实现。
  4. LED由GPIO驱动,其中与业务操作无关的内核线程自动控制的LED仍然留在内核,而通过ioctl提供给用户层的LED可以移动到用户空间控制。
  5. I2C,DAC,ADC,USB,EEPROM和全局时钟都属于前面提到的实现了只有内核驱动支持的复杂协议,所以要保留在内核不做改变。
  6. 电源指的是A7和USERFPGA的上下电管理功能,由于电源异常时需要SCM在几十毫秒之内响应,所以这些逻辑可以选择保留在内核里,或者单独实现为一个守护进程,开机常驻后台,SCMservic通过进程间通信控制守护进程(如果将SCMservic本身视为一个不会关闭的守护进程则不需要,这里考虑到了调试时SCMservic可能会被手动关闭的场景),又或者将上电机制移动到用户空间,而下电机制保留在线内核。
  7. PWM用于风扇控制需要一个后台常驻的执行流,可以是内核线程或者一个守护进程(如果将SCMservic本身视为一个不会关闭的守护进程则不需要额外的守护进程)。另外目前风扇转速由电源管理模块控制,所以这两个要一起考量。

(2)S8驱动功能简介

  1. master构建了一个struct cell_bottom结构代表SCM板,下挂很多个指针,每个指针由各种子模块初始化时填充,同时export一个全局接口使得所有模块都可以访问所有资源;master还提供大部分子模块的ioctl入口; 特殊的,这个cell_bottom结构还基于设备树用复杂的C代码维护了一套连接器名称,偏移以及连接器所使用电压挡位值的数组,这种复杂的字符串操作没有必要放在内核空间去做,但由于这个功能是连接器设置的基础,如果要迁移到用户空间的话,要十分小心的处理所有依赖。

  2. Cell tree这个模块并不是一个常规的驱动,而是聚合了大量的模块与连接器相关操作的utility模块,其中一部分功能只是返回字符串,如top_get_port_voltage_name获取对应连接器电压挡位的名称,因为内核并不适合做类似的字符串操作,所以应该将这部分代码都移动到用户空间;另外涉及GPIO操作和从内核空间读取用户空间/sys目录信息的部分也要移动到用户空间。

  3. clk-connect-output通过GPIO操作时钟源输出,应该移动到用户空间

  4. clock-mux通过GPIO操作18路时钟选择,应该移动到用户空间

  5. clk-si570/si5338时钟设备驱动,没有好的用户空间替代,保留在内核空间

  6. TCA9548 Linux内核标准的I2C SWITCH驱动,不做改变

  7. DAC基于I2C的DAC模块,保留在内核空间

  8. ADC模块(MCP3422)基于I2C的Linux内核标准ADC驱动,不做改变

  9. Daughter模块基于I2C的子卡EEPROM读写模块,保留在内核空间

  10. FPGA模块基于内存映射实现bitstream配置,FPGA ID读取,FPGA时钟设置/获取,LED灯的控制,FPGA上电后的初始化回调等。在内核中实现的bitstream配置代码十分复杂,不利于扩展和维护,应该移动到用户空间,这个功能的工作量比较大,可以放到后面做。其余的LED控制,FPGA信息读取和设置也可以移动到用户空间。

  11. Freq-detector570频率检测模块,通过debugfs输出570信息,用于调试不开放给用户,不需要变动。

  12. GPIO Controller模块GPIO controller驱动,提供基础的GPIO功能,保留在内核

  13. S2C-I2C模块I2C设备驱动,提供基本的I2C功能,保留在内核

  14. IRQ模块提供多路中断聚合功能,保留在内核里。

  15. Keyboard模块按键检测模块,依赖GPIOController在中断聚合模块中注册的中断,可以将中断转发到用户空间去处理,可以迁移到用户空间。

  16. LED模块总共有3个驱动,u-boot-led,ps-led,s2c-led,前两个是编译到内核开机过程中使用的,第3个是开机后加载并在系统运行过程中使用的,可以将s2c-led中只由用户空间控制的LED操作迁移到用户空间。

  17. PWM模块用于风扇控制。为了保证用户进程关闭的情况下风扇仍然受控,保留在内核空间或者移动到一个守护进程中。

  18. Firewall模块基于内存映射的A7进行上下电控制,这个模块应跟随上下电模块决定是否迁移。

  19. pwr-mgr模块控制上板A7和USERFPGA的上下电,同时根据上电的stage和power控制每个板的风扇转速,保留在内核空间或者移动到一个守护进程中。

  20. Pwr-s8模块控制每个上板的上下电,输出相应状态,是pwr-mgr的子模块。

  21. Reset模块基于内存映射实现复位脉冲等功能,应该移动到用户空间

  22. Ringbus模块基于内存映射实现ringbus功能。值得注意的是ringbus内核层用锁来防止多个执行流并行访问ringbus设备,如果将其移动到应用程序,则若能保证只有SCMservice访问ringbus就只需普通的mutex,若需要保证多进程能够访问ringbus就需要跨进程的同步机制保护。

  23. UCD9000电压电流监控模块,相关信息输出到/sys目录下,保留在内核

  24. decrypt模块U-BOOT开机时将加密的厂商信息从FLASH拷贝到固定的物理地址,再由decrypt模块解密后供其他模块使用。这个功能涉及产品基础,有待讨论。

  25. USB模块保留在内核

  26. 还有一些Linux内核标准的设备驱动未列出。

(3)关于debugfs的条目

S8驱动很多模块都将一部分信息导出到debugfs,使得用户可以在linux命令行读取和设置gpio,风扇,连接器电压和操作top板上下电等。这个功能在service和pprt之外提供了另一条访问资源的通道,但如果service和pprt能保证稳定运行的话,似乎本功能提供的冗余没有必要。可以先保留在内核中,之后开发或迁移某个模块时逐条检查该模块对应的debugfs,看是否需要,如需要则提供用户空间的替代或者保留debugfs条目,否则就删除。

(4)关于上电初始化回调

上板上电时,先给a7上电,然后调用依赖a7核心模组的驱动模块注册的初始化回调,重新初始化对应模块,初始化和使能连接器,最后给USERFPGA上电。
如果上电机制保留在内核,则为了给被移动到用户空间的模块初始化的机会,可以分阶段先上电a7,再调用用户空间的初始化函数,接着初始化和使能连接器,最后给USERFPGA上电。目前被移动到用户空间且具有上电初始化回调的模块有FPGA模块,Ringbus模块。
如果上电机制被移动到用户空间,则为了给留在内核的驱动初始化的机会,也要分阶段上电,同时需要重新初始化的内核驱动提供ioctl选项供应用层调用。这种驱动目前有clk-si570,daughter,freq-detect,gpio controller,s2c-i2c,irq, ucd9000。

阅读全文

CI

2024/7/8

关于CI技术分享

1.介绍GitLab CI:
GitLab CI(Continuous Integration)是 GitLab 提供的集成持续集成和持续交付(CI/CD)功能的一部分。它允许开发团队自动化构建、测试和部署他们的代码,以便更快地交付高质量的软件。通过 GitLab CI,你可以在 GitLab 仓库中配置一个 .gitlab-ci.yml 文件,其中定义了构建、测试和部署的各个阶段和步骤。当你的代码提交到 GitLab 仓库时,GitLab CI 会自动执行你定义的流程,并提供实时的构建和测试结果。
一些 GitLab CI 的主要功能包括:
⦁ 自动构建和测试:当代码发生变更时,GitLab CI 可以自动执行构建和测试任务,确保新的代码不会引入错误。
⦁ 并行和分布式处理:GitLab CI 允许你同时在多个运行器上运行测试和构建任务,加速整个流程。
⦁ 容器化支持:GitLab CI 支持使用容器来构建和运行代码,提供更一致的开发和测试环境。
⦁ 持续交付和部署:你可以配置 GitLab CI 来自动将测试通过的代码部署到生产环境,实现持续交付和持续部署。
⦁ 自定义流程:通过 .gitlab-ci.yml 文件,你可以定义自己的构建和测试流程,以适应你的项目需求

  1. 为什么使用GitLab CI:
    自动化构建和测试:GitLab CI 允许你在代码提交后自动执行构建和测试任务。这有助于及早发现和解决代码错误,确保代码的稳定性和质量。
    ⦁ 快速反馈:自动构建和测试意味着开发人员可以更快地获得关于他们提交的代码质量的反馈。这有助于防止问题在代码库中累积,提高了开发效率。
    ⦁ 持续集成:通过持续集成,团队可以频繁地集成代码,减少分支间的差异,避免“集成地狱”。这有助于减少代码冲突和解决集成问题的时间。
    ⦁ 自动化部署:GitLab CI 不仅限于构建和测试,还可以自动部署代码到不同环境,如开发、测试和生产环境。这样可以实现持续交付和持续部署,减少人工干预。
    ⦁ 一致的构建环境:GitLab CI 支持容器化,确保构建和测试在一致的环境中运行。这可以避免因为开发和测试环境的差异导致的问题。
    ⦁ 跨平台支持:GitLab CI 可以用于各种不同的项目和编程语言,支持跨平台开发和部署。

  2. 基本概念和术语:
    Runner(执行者):Runner是一个在项目中运行CI/CD作业的实体。它可以是物理服务器、虚拟机、容器等。Runner负责接收来自GitLab的作业请求,并在其上运行这些作业的脚本。有两种类型的Runner:共享Runner(Shared Runner)是由GitLab托管的,可供所有项目使用;特定项目的Runner(Specific Runner)则专门为特定项目配置和使用。

⦁ Job(作业):Job是CI/CD工作的最小单元。它是在Runner上执行的一个命令序列,通常是编译、测试、构建或部署等任务。在一个CI/CD流程中,可以定义多个作业,每个作业都有一个唯一的名称,以及一个要运行的脚本或命令。

⦁ Pipeline(流水线):Pipeline是一组有序的作业,按照定义的顺序依次执行。每当您推送代码或发生其他触发条件时,GitLab会创建一个新的流水线。流水线可以包含一个或多个阶段(Stage),用于将作业分组,以便您可以在流水线的不同阶段执行不同类型的任务。

⦁ Stage(阶段):阶段是流水线中的一个逻辑分组,用于将相关作业组合在一起。一个流水线可以包含多个阶段,每个阶段可以包含一个或多个作业。通常,阶段用于将具有相似目标的作业分组,例如构建、测试、部署等。
Stage包含一下三个关键字:
⦁ Build(构建):构建阶段涉及将源代码转换为可执行的应用程序、库或软件包。在这个阶段,通常会执行编译、打包、生成可执行文件等操作,以创建软件的基本构建版本。构建后的产物可以是编译后的代码、二进制文件、库等。构建阶段的目标是确保代码能够成功编译,并且构建过程是可重复的。

⦁ Test(测试):测试阶段用于验证构建的软件在不同方面的正确性和稳定性。这包括单元测试、集成测试、端到端测试等。测试的目标是捕获和纠正代码中的错误、漏洞和问题,确保软件在各种情况下都能按预期工作。测试阶段是保障软件质量的关键一步,它帮助开发者及早发现并修复潜在的问题。
⦁ Deploy(部署):部署阶段涉及将经过测试和验证的软件发布到目标环境中,使其对用户可用。这可能包括将应用程序部署到服务器、云平台、移动设备等。部署通常还涉及配置服务器、设置环境变量、安装依赖项等任务,以确保应用程序能够正确运行。部署阶段的目标是将经过验证的软件交付给最终用户,以实现持续交付和持续部署的目标。
下图是目前runtime_version_seperate分支上的gitlab-ci.yml文件的部分内容,这部分内容主要是做ut覆盖率的检查。可以通过下面的例子看出一个完整的job是如何组织的;

学习更多参考官方文档:
gitlab-ci-yaml.md (fennay.github.io)
GitLab Pages | GitLab

阅读全文

2024/7/5

hex blog creat and push

  1. 进入目录blog
  2. 使用命令 hexo new "xxxx"
  3. 使用命令清除缓存 hexo clean
  4. 使用命令生成 hexo g
  5. 使用命令推送 hexo d

参考 保姆级教学 基于Hexo搭建个人网站(Github)_github 网站-CSDN博客

阅读全文

2024/7/5

USB使用模型设计

send

+———————-+
| Open USB Device |
| (open) |
+———-+———–+
|
v
+—————+
| USB Reset |
| (usb_reset) |
+——-+——-+
|
v
+—————+
| Get EP Max |
| (usb_get_epmax)|
+——-+——-+
|
v
+—————+
| Check Endpoint |
| Validity |
+——-+——-+
|
v
+——+———+
| Set Timeout |
| Loop |
+——+———+
|
v
+—–+——–+
| Wait TX Buf |
| (usb_wait_tx_buf)|
+——+——–+
|
+——v——-+
| Timeout? |
+——+——-+
|
+——+——-+ (No)
| Yes |
| Continue <-+
+——+——-+
|
v
+——+——-+
| Copy Data |
| to Buffer |
+——+——-+
|
v
+——+——-+
| Submit TX Req|
|(usb_submit_tx)|
+——+——-+
|
+——+——-+ (Success?)
| Yes |
| Loop |
+——+——-+
|
+——+——-+ (No)
| Failure |
| Exit Loop |
+——+——-+
|
v
+—————+
| Close Device |
| (close) |
+—————+

rec

  1. 打开USB设备文件 /dev/usb/scm0。
  2. 如果打开失败,输出错误信息并退出。
  3. 使用 mmap 函数映射共享内存区域,用于读取数据。
  4. 调用 usb_get_epmax 函数获取设备的最大端点数。
  5. 如果获取失败,输出错误信息并退出。
  6. 调用 usb_reset 函数对USB设备进行软复位。
  7. 设置超时时间为1秒。
  8. 进入接收数据的循环。
  9. 调用 usb_wait_rx_buf 函数等待接收缓冲区中的数据。
  10. 如果超时,延迟10毫秒并继续下一次循环。
  11. 如果出现其他错误,输出错误信息并退出循环。
  12. 如果接收到的缓冲区索引或端点索引超出范围,输出错误信息并退出循环。
  13. 如果是第一次接收到数据,记录开始时间。
  14. 计算出接收到数据的目标地址。
  15. 如果端点对应的文件指针为空,创建一个以端点索引命名的文件,并打开以供写入。
  16. 更新接收数据的长度。
  17. 将数据写入到相应的文件中。
  18. 调用 usb_return_rx_buf 函数归还接收缓冲区。
  19. 如果接收到的数据长度不满一个缓冲区大小,则关闭文件,记录结束时间,并计算接收数据的时长和传输速度。
  20. 遍历所有端点,关闭所有已经打开的文件。
  21. 解除共享内存的映射。
  22. 关闭USB设备文件。
  23. 输出退出信息并返回0。

流程图: 接收数据

+—————–+ +———————-+
| 开始 | | 打开USB设备文件 |
+—————–+ +———————-+
| |
v |
+—————–+ +———————-+
| 打开失败 | | mmap共享内存 |
+—————–+ +———————-+
| |
v |
+—————–+ +———————-+
| 获取最大端点数 | | usb_get_epmax |
+—————–+ +———————-+
| |
v |
+—————–+ +———————-+
| 获取失败 | | 调用usb_reset |
+—————–+ +———————-+
| |
v |
+—————–+ +———————-+
| 设置超时时间 | | 进入接收数据的循环 |
+—————–+ +———————-+
| |
v |
+—————–+ +———————-+
| 进入循环 |————–>| 调用usb_wait_rx_buf |
+—————–+ +———————-+
| 接收成功、超时、其他错误 |
v |
+—————–+ +———————-+
| 处理接收数据 | | 判断是否超时 |
+—————–+ +———————-+
| 接收成功、超时、其他错误 |
v |
+—————–+ +———————-+
| 判断索引范围 | | 判断是否超时 |
+—————–+ +———————-+
| 超出范围、其他错误 |
v |
+—————–+ +———————-+
| 第一次接收数据 | | 计算数据目标地址 |
+—————–+ +———————-+
| |
v |
+—————–+ +———————-+
| 计算目标地址 | | 检查文件指针是否为空 |
+—————–+ +———————-+
| |
v |
+—————–+ +———————-+
| 文件指针为空 | | 更新接收数据长度 |
+—————–+ +———————-+
| |
v |
+—————–+ +———————-+
| 更新数据长度 | | 写入数据到文件 |
+—————–+ +———————-+
| |
v |
+—————–+ +———————-+
| 写入文件 | | 归还接收缓冲区 |
+—————–+ +———————-+
| 数据长度不满一缓冲区大小 |
v |
+—————–+ +———————-+
| 数据长度不满 |————–>| 关闭文件 |
+—————–+ +———————-+
| |
v |
+—————–+ +———————-+
| 遍历所有端点 | | 解除共享内存映射 |
+—————–+ +———————-+
| |
v |
+—————–+ +———————-+
| 解除映射 | | 关闭设备文件 |
+—————–+ +———————-+
| |
v |
+—————–+ +———————-+
| 关闭设备文件 | | 输出退出信息 |
+—————–+ +———————-+
| |
v |
+—————–+ +———————-+
| 输出退出信息 | | 返回0 |
+—————–+ +———————-+

一、工作原理

1.根据你提供的工作原理,我理解你需要在Host端和Device端分别运行USBService的实例来支持传输层的握手机制。在Host端,USBService会作为RT客户端的后台线程运行,称为ServerProxy;在Device端,USBService会作为server的后台线程运行,称为HostProxy。

工作流程如下:

  1. 当RT需要连接SCM server时,它会通知ServerProxy创建一个本地TCP server,并连接到该server。同时,ServerProxy也会尝试打开对应的USB节点。如果USB节点打开失败或者创建TCP server失败,则连接建立失败。
  2. 接着,ServerProxy通过USB传输层协议与HostProxy建立连接,并设置正确的读写权限。
  3. 之后,ServerProxy开始转发RT与SCM server之间的数据。
  4. 当RT退出或主动关闭时,只需关闭TCP socket。ServerProxy会感知到TCP socket关闭,并通过USB传输层协议与HostProxy断开连接。如果连接具有写权限,ServerProxy也会正确地放弃该权限。最后,关闭USB节点和本地TCP server的监听套接字。

关闭监听套接字的原因是因为在仅支持单连接的前提下,如果不关闭监听套接字,其他试图连接SCM server的RT无法监听ServerProxy的已知端口。另外,USB节点也有仅支持单用户打开的限制,所以也应关闭。

这个工作流程涉及到TCP连接的管理、USB传输层协议的建立与断开、权限管理等多个方面的操作。需要确保在各种情况下都能正确处理连接的建立和断开,并正确地管理权限和资源释放。

2.根据你提供的需求,USBService需要像TCP一样定义握手建立连接和挥手关闭连接的过程,并且需要定义额外的传输层协议。你可以像TCP一样定义传输层协议,将用户的数据封装进来。这样做的好处是与业务层解耦,方便扩展更多的传输层功能,但缺点是额外的传输层包头会消耗一部分带宽。

传输层包头可以定义如下:

  • 4字节的MAGIC
  • 4位Reserved
  • RST:表示连接重置
  • FIN:表示连接结束
  • ACK:表示应答
  • SYN:表示连接建立

每个标志位只占用1位。USB只提供单连接,因此不需要端口号区分双方身份。传输协议层不提供数据完整性保证,因此不需要校验和。

包类型可以定义为:

  • 数据包(DATA),其值为RST、FIN、ACK、SYN全为0
  • 连接建立包(SYN),其值为SYN=1
  • 应答包(ACK),其值为ACK=1
  • 连接结束包(FIN),其值为FIN=1
  • 连接重置包(RST),其值为RST=1

为了简化设计,不会像TCP那样在带有SYN、ACK、FIN的包中携带有效数据。

通过这样的定义,USBService就可以提供连接机制,并能够在Host端和Device端之间建立和关闭连接。这种设计既能满足需求,又能保持较小的传输开销。

3.连接关闭步骤被简化为2次挥手,被动方等待超时后直接退出。这种简化是因为引入重传的逻辑会使代码复杂化,并且可能会导致RT卡住一段时间,对用户不太友好。而且,没有正确关闭连接的代价相对较小。

针对HostProxy和ServerProxy连接没有正确关闭的情况,处理措施如下:

  • 如果Server端HostProxy的连接没有正确关闭,则应用层心跳包超时会触发连接关闭。USB物理连接断开或新的连接到达时也会正确释放旧连接,这样可以保证板子资源的可用性。
  • 如果Host端ServerProxy的连接没有正确关闭,则当ServerProxy在本应关闭的连接上尝试传输数据时,HostProxy会销毁连接并返回错误。

通过这样的处理措施,可以确保在连接未正确关闭的情况下,仍然能够释放旧连接并防止资源的长时间占用,从而保证系统的稳定性和可靠性。

阅读全文
1
avatar
ranyue

Description