PVE

PVE的备份和恢复

进入pve管理后台,在各虚拟机的备份选项点击备份

备份文件路径是Var/lib/vz/dump,下载到其他地方保持

需要恢复时将备份文件上传到Var/lib/vz/dump,在存储 local里点击对应的备份文件进行还原,然后启动虚拟机

!!!如果启动虚拟机时提示找不到iso镜像,找到硬件选项,编辑CD/DVD驱动器重新选择一下iso镜像文件

pve直通amd核显没法玩

直接装fedora,然后用fedora的虚拟机装pve

逆向api通过外挂解锁特殊功能

逆向api通过外挂解锁特殊功能

很多时候逆向得到的api只能进行文本对话,不能实现最新的ai特性或者协议。比如function call功能 MCP协议 谷歌最新的a2a协议
我经过实践和尝试,提炼了一套可行的思路和代码,使得逆向api一样可以支持最新的ai相关的特性或协议(目前仅仅尝试了function call和mcp,a2a还需要了解)

function call和Mcp的本质以及异同

1.function call

是llm的具体能力,专注于单个外部工具的调用生成

2.Mcp

一种更上层的宏观的系统设计,通常包含多个function call

异同

打个比方,function call是锤子,剪刀等工具,而Mcp就是说明书之类的东西,指导如何用锤子剪刀等工具制造东西
MCP负责决定在什么时候、为什么以及如何组合使用这些“动作”(包括Function Calling、内部思考、信息检索等)来达成最终目标


外挂的实现思路

fc(function call简称)或者Mcp说通俗点就是,llm根据tools参数和上下文判断要不要从上下文中提取参数执行fc或者Mcp.
不管是fc还是Mcp,最终的执行都不是说ai做的,而是ai返回一个结构化(通常是JSON格式)的指令,然后客户端或者别的什么端进行运算.而ai恰好就可以通过**prompt(提示词)**做到这种效果。
因为我们的思路时,在中转或者2api的服务端先过滤判断一次.
如果需要fc或者mcp,通过另外一轮ai对话提炼参数,把返回的参数加入之前的ai对话中,返回进行fc或者Mcp的运算结果.
接收到运算结果后得把结果重新编排并再进行一次对话,添加一个简单的提示词—–比如:根据上下文回答问题

大致思路流程如图

具体实现

需要一个question变量存储用户的问题,一个标志变量tools_use表示是用过了工具
是否需要使用工具我是通过提示词让ai不需要使用工具的时候就输出no

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# 我的判断是否需要使用工具的提示词
<system>
你是一个专业的 AI 助手,擅长使用各种工具解决问题。你将以专业、友好的态度与用户交流,提供准确和有价值的帮助。
</system>
<persona>

- 专业性:你具备丰富的技术知识和经验
- 思维方式:逻辑清晰,善于分析问题
- 沟通风格:友好、耐心,注重用户体验
- 工作态度:严谨认真,注重细节
</persona>

<instructions>
1. 分析用户需求:
- 仔细理解json格式的工具列表和用户的问题
- 确定需不需要调用工具,需要调用哪些工具以及调用该工具需要的参数

2. 工具使用规范:

- 每次可调用多个工具,如果没有依赖关系,尽量同时返回全部工具的参数,如果有依赖关系,就依次调用
- 如果用户明确要求依次调用才依次调用
- 确保工具调用格式正确,参数完整
3. 回答质量要求:

- 提供清晰、准确、结构化的答案
- 承认知识限制,不随意猜测或编造
- 如果需要使用工具,则返回结果必须严格使用json格式,返回结果不能有其他文字,只返回json格式
- 如果不需要使用工具返回结果不能有其他文字,只返回"no"
4.举例
例子一
用户输入以下内容:
[
{
"type": "function",
"function": {
"name": "fd71edc4f65924613b9fd8330e78eb243",
"description": "获取中国国内的天气预报",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "逗号分隔的经纬度信息 (e.g., 116.40,39.90)"
},
"days": {
"type": "string",
"enum": [
"now",
"24h",
"72h",
"168h",
"3d",
"7d",
"10d",
"15d",
"30d"
],
"description": "预报天数,now为实时天气,24h为24小时预报,72h为72小时预报,168h为168小时预报,3d为3天预报,以此类推"
}
}
}
}
}
]杭州的天气怎么样?
你需要返回的内容是:
[
{
"id": "call_M4eYNv6oPaLunpZLe1iWdfiK",
"function": {
"name": "fd71edc4f65924613b9fd8330e78eb243",
"arguments": "{\"days\":\"now\",\"location\":\"121,31\"}"
},
"type": "function"
}
]
}

例子二
用户输入以下内容:
[
{
"type": "function",
"function": {
"name": "fd71edc4f65924613b9fd8330e78eb243",
"description": "获取中国国内的天气预报",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "逗号分隔的经纬度信息 (e.g., 116.40,39.90)"
},
"days": {
"type": "string",
"enum": [
"now",
"24h",
"72h",
"168h",
"3d",
"7d",
"10d",
"15d",
"30d"
],
"description": "预报天数,now为实时天气,24h为24小时预报,72h为72小时预报,168h为168小时预报,3d为3天预报,以此类推"
}
}
}
}
}
]你好
你需要返回的内容是:
no

代码逻辑

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
tools_use = False
tools_answer = 'no'

tools_question = str(tools_json) + messages[-1]["content"] # 缓存用户问题

tools_json = data.get("tools", None) #提取tools参数
# 首先用tools_json和question以及提示词进行新的对话判断是否需要工具
# 需要的话返回的是结构化的调用参数,不需要就返回的no
# 这轮新的对话可以使用2api的模型也可以使用正规的官方api

# 只有tools_answer不是no且tools_use是false时才说需要mcp或者fc且之前没有进行过工具的调用
if tools_answer != 'no' and not tools_use:
# 需要function call
# 构造openai api格式的返回
# 生成created
current_timestamp = int(time.time())
created = current_timestamp
# 下面是告诉客户端需要调用mcp的返回值模板,看了L站n多关于mcp的帖子才知道
completion = {
"id": answer_id,
"object": "chat.completion",
"created": created,
"model": model,
"system_fingerprint": system_fingerprint,
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"tool_calls": json.loads(tools_answer) if tools_answer else None,
"content": None, # 如果有提示词能达到claude使用MCP的思考过程的效果,可以把模拟的思考过程赋值到此处
},
"logprobs": None,
"finish_reason": "tool_call"
}],
"usage": {
"prompt_tokens": prompt_tokens,
"completion_tokens": completion_tokens,
"total_tokens": total_tokens
}
}
tools_answer = 'no' # 重置相关参数,便于下次正确使用



# 判断上下文最后的messages的role角色,如果是tools,说明这是客户端发回来的mcp或者fc调用的结果
elif messages[-1]["role"] == 'tool':

#需要从头到位遍历messages,把tool的role转成user,这样逆向的api才好使用
# tools的结果在content里的第一个的text里面
messages[-1]["content"][0]["text"]


# 同时判断tools_use是不是true, 是说明这就是本次tools的调用结果
if tools_use:
# 这里需要保证对话的最后一条是用户起初提的问题,不同的逆向api这里的代码不一样比如
chat_messages["content"] = question
question = None# 恢复变量和标志位
tools_use = False

MCP的思考过程

我也写了一个粗略的提示词想要达成这种效果,但由于我的提示词功力不够以及逆向的模型过于垃圾,导致效果不理想

end

这样做了以后就可以让逆向api支持mcp协议了
经过openwebui几个简单的工具测试可以达成效果
总结一下,MCP并不是什么困难的东西,只不过批了一层外衣,MCP的模拟过程实质还是用提示词的使用,只要了解了MCP过程中的api返回值接受值的格式就可以轻松地给任意模型加上MCP
未来的llm发展我猜测为两个方向
融合和外挂

融合

就是MOE模型里进一步增加从对话中提取参数结构化输出的能力

外挂

就是主模型增强关于是否需要MCP的判断能力,需要的话交给一个极致轻量的次生模型提取参数并结构化输出。由于次生模型只需要提取参数结构化输出,是可以做到轻量且高效准确的,主模型就可以完全抛弃这部分能力,增强其他能力

补充

下一步了解下谷歌的a2a协议

8845-all in one

其他X86多网口设备均可以参考

方案一

PVE做底层,其他全部用虚拟机解决

直通核显屡次尝试失败,要么开始就不能用,要么用一会就不行了

要么直通成功,没问题了,但是测试虚拟机里安装rocm,无法使用780m核显,且vram占用一直为0

方案二

PVE直通核显失败

于是我用fedora做实体机,然后虚拟机里运行PVE

需要gpu的服务都放fedora实体机上

不需要的都放虚拟机里的PVE,这样即使重装也只需要恢复使用gpu的服务

我需要使用gpu的服务有comfyui, 影视(jellyfin或者emby等选一个,其实用的也不多,基本上看alist就行了)

第一步

创建网桥

必须双网口及以上才行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 我的两个网口分别是 enp2s0 enp3s0    使用enp2s0作为网桥   切换到root设置更方便
nmcli connection add type bridge-slave con-name pve-port1 ifname enp2s0 master Pve
# pve-port1和Pve可以自定义, enp2s0是你需要作为网桥的设备名称
nmcli connection modify pve ipv4.addresses 192.168.31.43/24
# 192.168.31.43/24 网桥的ip
nmcli connection modify pve ipv4.gateway 192.168.31.1
# 网桥的网关
nmcli connection modify pve ipv4.dns "8.8.8.8"
# 网桥的DNS
nmcli connection modify pve ipv4.method manual
# 切换网桥为手动配置
nmcli connection up pve
# 启动网桥
firewall-cmd --add-interface=pve --zone=trusted --permanent
# 无条件放行网桥流量

第二步

使用fedora的libvirt创建PVE虚拟机

1
dnf install @virtualization

可以使用virt-manager以gui方式创建虚拟机

网络源选桥接设备 设备名称填网桥名称

第三步

直通需要的设备

pci设备直通到PVE虚拟机后,无法再直通到PVE下的虚拟机,因为PVE无法开启iommu

但我要直通的pci设备只有m2转sata的转接卡

只需要把它直通给PVE就行了

然后通过scsi把硬盘块直通给虚拟机就行

1
2
3
scsi1: /dev/sdb,cache=directsync,discard=on,iothread=1
scsi2: /dev/sda,cache=directsync,discard=on,iothread=1
# 更多参数询问ai

至此,all in one完成,后续需要加服务,不需要gpu添加到虚拟机PVE

需要GPU的直接添加到宿主机,把宿主机服务的部署整理成脚本,重置后一键恢复服务

GPU失败是因为驱动问题

解决驱动问题

卸载vfio

Next-chat-web同步

  1. 修改docker的环境变量
1
WHITE_WEBDEV_ENDPOINTS=https://alist.leioukupo.top/机械盘
  1. 在机械盘目录下新建文件夹和文件
1
2
3
机械盘/
└── chatgpt-next-web
└── backup.json
  1. 配置next chat
1
2
3
# 必须启用代理
# 代理地址空着
webdev地址 : https://alist.leioukupo.top/dav/机械盘

3588Pve实践

Rock 5b卖掉后在咸鱼重新入手了一块3588,8+32的配置

Pve安装

需要debian系统

1
curl -L https://mirrors.apqa.cn/proxmox/debian/pveport.gpg | sudo tee /usr/share/keyrings/pveport.gpg >/dev/null
1
echo "deb [deb=arm64 signed-by=/usr/share/keyrings/pveport.gpg] https://mirrors.apqa.cn/proxmox/debian/pve bookworm port" | sudo tee  /etc/apt/sources.list.d/pveport.list
  1. 编辑/etc/hosts
1
2
3
4
5
6
7
8
127.0.0.1   localhost
127.0.1.1 yjh-jm10-3588
::1 localhost yjh-jm10-3588 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
192.168.31.43 proxmox-server yjh-jm10-3588
1
2
sudo apt install ifupdown2
sudo apt install proxmox-ve postfix open-iscsi pve-edk2-firmware-aarch64

最后要记得重启,在访问ip+8006就可以访问pve管理界面了,密码和账号是root和对应的密码

虚拟机测试

使用非lxc容器方式

注意点

  • cpu设置要勾选高级选项,在cpu绑定一栏输入 4,5,6,7
  • 无显示输出,需要按照pve官方文档操作

Pve官方文档

1
2
apt download pve-edk2-firmware-aarch64=3.20220526-rockchip
dpkg -i pve-edk2-firmware-aarch64_3.20220526-rockchip_all.deb

如果apt下载失败,复制pve-edk2到浏览器下载

虚拟机硬件里的显示需要选择ramfb方式

安装istore

使用虚拟机安装

1
2
qemu-img convert -f raw -O qcow2 istoreos-22.03.7-2024122712-h88k-squashfs-combined.img vm-101-disk-1.qcow2
qm importdisk 101 vm-101-disk-1.qcow2 local

打开刚才创建的虚拟机的硬件,会看到有个未使用的磁盘,双击打开,点确认即可

对应编辑一下引导顺序。
启动后,是uefi shell,没有进入istore界面,需要带efi的istore固件

lxc运行openwrt

1.先上传lxc模板,再执行下面的命令

1
2
3
4
5
6
7
8
9
10
11
12
13
pct create 101 \
local:vztmpl/rootfs.tar.xz \
--rootfs local:1 \
--ostype unmanaged \
--hostname openwrt \
--arch arm64 \
--cores 8 \
--memory 1024 \
--swap 0 \
-net0 bridge=vmbr0,name=eth0


# rootfs.tar.xz根据你的lxc模板名字修改
1
2
3
101是id
local:1的1是指该lxc容器磁盘大小是1g
memory 运行内存

2.修改lxc容器配置文件

1
nano /etc/pve/lxc/101.conf
1
2
3
4
5
6
7
8
9
10
11
arch: arm64
cores: 8
features: nesting=1
hostname: openwrt
memory: 1024
net0: name=eth0,bridge=vmbr0,hwaddr=BC:24:11:BE:DB:C8,type=veth
ostype: unmanaged
rootfs: local:101/vm-101-disk-0.raw,size=1G
swap: 0
lxc.mount.entry: /dev/ppp /dev/ppp none bind,create=file
lxc.mount.entry: /dev/net/tun /dev/met/tun none bind,create=file

3.在pve的web页面找到这个lxc容器,选项—》功能 —-》勾选嵌套

4.启动

1
pct start 101

4.1修改/etc/config/firewall,后面新增

1
2
lxc-attach 101 
# 进入op终端,用vi编辑文件
1
2
3
4
5
6
7
8
9
10
11
12
config rule
option name Allow-Web-WAN
option src wan
option dest_port 443
option proto tcp
option target ACCEPT
config rule
option name Allow-Web-WAN
option src wan
option dest_port 80
option proto tcp
option target ACCEPT

4.2.重启防火墙

1
/etc/init.d/firewall restart
1
ifconfig

根据ifconfig显示的ip访问就可以进入后台

参考链接

openwrt_lxc模板

pve开启ipv6

https://www.icn.ink/pve/57.html

Qt笔记

Qt笔记

信号

自定义信号

1
2
signals:
void sig_addOne(int value);

使用signals声明,返回值是void

emit—-发送信号

1
emit sig_add(socre++);

Qt的子线程无法直接修改ui,需要发送信号到ui线程进行修改

非基础类型参数注册

1
2
qRegisterMetaType<Score>("Score");
qRegisterMetaType<string>("string");

槽函数的参数和信号参数的关系

Qt槽函数的参数需要和信号的参数保持一致, 可以比信号的参数少, 但是不能顺序不同, 也不能比信号的参数多

信号重名如何处理

使用泛型解决信号重载问题

1
2
3
4
connect(ui->comboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &Widget::onIndex);
connect(ui->comboBox, QOverload<const QString&>::of(&QComboBox::currentIndexChanged),
this, &Widget::onIndexStr);

信号和槽函数关系

  • 信号是由对象在某个特定事件发生时发出的消息。信号本身并不执行任何操作,它仅仅是表示某种状态的变化或事件的发生。例如,当一个按钮被点击时,它会发出一个clicked()信号
  • 槽是一个函数,可以接收并处理信号。当一个信号被发出时,与该信号连接的槽函数就会被调用。槽函数可以是任何成员函数、全局函数或lambda表达式

ADC使用

stm32的ADC单通道单次使用

ADC通道与引脚对应关系

adc通道与对应的引脚
stm32f1没有内部温度传感器

单次ADC采集流程

1.开启对应时钟和ADC1时钟,设置PA1为模拟输入

1
2
3
4
5
6
7
8
9
10
// 开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // GPIOA的时钟定义错误,应就近挂在APB2上,而不能图省事直接挂在AHB上
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 设置ADC时钟,设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14M
// 初始化GPIO
GPIO_InitTypeDef ADC_PA1;
ADC_PA1.GPIO_Pin = GPIO_Pin_1;
ADC_PA1.GPIO_Mode = GPIO_Mode_AIN;// 模拟输入模式
ADC_PA1.GPIO_Speed = GPIO_Speed_50MHz;// 50MHz
GPIO_Init(GPIOA, &ADC_PA1);

2.复位ADC1,同时设置ADC1参数

1
2
3
4
5
6
7
8
ADC_DeInit(ADC1);
adc1_1.ADC_Mode = ADC_Mode_Independent;// adc工作模式 独立模式
adc1_1.ADC_ContinuousConvMode = DISABLE;// 连续转换是否开启
adc1_1.ADC_DataAlign = ADC_DataAlign_Right;// 数据对齐方式
adc1_1.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;// 触发转换方式 软件触发
adc1_1.ADC_ScanConvMode = DISABLE;// 扫描模式是否开启
adc1_1.ADC_NbrOfChannel = 1;// 转换通道数目
ADC_Init(ADC1, &adc1_1);

3.使能ADC

1
ADC_Cmd(ADC1,ENABLE);

4.配置规则通道参数

1
2
ADC_RegularChannelConfig(ADC1, channel, 1, ADC_SampleTime_7Cycles5); // 通道的转换顺序 如果设置Rank为1
//那么这个通道将会首先被转换

5.开启软件转换

1
ADC_SoftwareStartConvCmd(ADC1,ENABLE); // 软件触发转换

6.等待转换完成,读取ADC值

1
ADC_GetConversionValue(ADC1);

RTC配置

RTC配置应用

日历配置流程

  1. 使能PWR时钟
1
RCC_APB1PeriphClockCmd()
  1. 使能后备寄存器访问
1
PWR_BackupAccessCmd()
  1. 配置RTC时钟源,使能RTC时钟
1
2
3
4
RCC_RTCCLKConfig()
RCC_RTCCLKCmd()
# 如果使用LSE 要打开LSE
# RCC_LSEConfig(RCC_LSE_ON)
  1. 初始化RTC(同步/异步分频系数和时钟格式)
1
RTC_Init ()
  1. 设置时间
1
RTC_SetTime ()
  1. 设置日期
1
RTC_SetDate()

闹钟配置流程

  1. 初始化RTC相关参数
  2. 关闭闹钟
1
RTC_AlarmCmd(RTC_Alarm_A,DISABLE)
  1. 配置闹钟参数
1
RTC_SetAlarm()
  1. 开启闹钟
1
RTC_AlarmCmd(RTC_Alarm_A,EABLE)
  1. 开启配置闹钟中断
1
2
3
RTC_ITConfig()
EXTI_Init()
NVIC_Init()
  1. 编写中断服务函数
1
RTC_Alarm_IRQHandler()

RTC周期性自动唤醒配置流程

  1. 初始化RTC相关参数
  2. 关闭WakeUp
1
RTC_WakeUpCmd(DISABLE)
  1. 配置WakeUp时钟分频系数/来源
1
RTC_WakeUpClockConfig()
  1. 设置WakeUp自动装载寄存器
1
RTC_SetWakeUpCounter()
  1. 使能WakeUp
1
RTC_WakeUpCmd( ENABLE);
  1. 开启配置闹钟中断
1
2
3
RTC_ITConfig()
EXTI_Init()
NVIC_Init()
  1. 编写中断服务函数
1
RTC_WKUP_IRQHandler();

Usmart

Usamrt流程

头文件内容解析

  • usmart.h—-usmart的主要函数
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
//8个函数函数
void usmart_init(u8 sysclk); // 初始化
u8 usmart_cmd_rec(u8 *str); // 识别
void usmart_exe(void); // 执行
void usmart_scan(void); // 扫描
u32 read_addr(u32 addr); // 读取指定地址的值
void write_addr(u32 addr, u32 val); // 在指定地址写入指定的值
u32 usmart_get_runtime(void); // 获取运行时间
void usmart_reset_runtime(void); // 复位运行时间
//两个结构体
// 函数名列表
struct _m_usmart_nametab
{
void *func; // 函数指针
const u8 *name; // 函数名(查找串)
};
// usmart控制管理器
struct _m_usmart_dev
{
struct _m_usmart_nametab *funs; // 函数名指针

void (*init)(u8); // 初始化
u8 (*cmd_rec)(u8 *str); // 识别函数名及参数
void (*exe)(void); // 执行
void (*scan)(void); // 扫描
u8 fnum; // 函数数量
u8 pnum; // 参数数量
u8 id; // 函数id
u8 sptype; // 参数显示类型(非字符串参数):0,10进制;1,16进制;
u16 parmtype; // 参数的类型
u8 plentbl[MAX_PARM]; // 每个参数的长度暂存表
u8 parm[PARM_LEN]; // 函数的参数
u8 runtimeflag; // 0,不统计函数执行时间;1,统计函数执行时间,注意:此功能必须在USMART_ENTIMX_SCAN使能的时候,才有用
u32 runtime; // 运行时间,单位:0.1ms,最大延时时间为定时器CNT值的2倍*0.1ms
};
//一些变量定义不需要解释
  • usmart_str.h—-usmart字符串的相关操作
1
2
3
4
5
6
7
8
u8 usmart_get_parmpos(u8 num);                                    // 得到某个参数在参数列里面的起始位置
u8 usmart_strcmp(u8 *str1, u8 *str2); // 对比两个字符串是否相等
u32 usmart_pow(u8 m, u8 n); // M^N次方
u8 usmart_str2num(u8 *str, u32 *res); // 字符串转为数字
u8 usmart_get_cmdname(u8 *str, u8 *cmdname, u8 *nlen, u8 maxlen); // 从str中得到指令名,并返回指令长度
u8 usmart_get_fname(u8 *str, u8 *fname, u8 *pnum, u8 *rval); // 从str中得到函数名
u8 usmart_get_aparm(u8 *str, u8 *fparm, u8 *ptype); // 从str中得到一个函数参数
u8 usmart_get_fparam(u8 *str, u8 *parn); // 得到str中所有的函数参数.

u8 usmart_get_fname

pcnt & 0x7f 应该表示第几个参数
strtemp每次循环指向一个字符
首先判断接收到的字符串前五个字符是不是void, 设置rval, 是不是需要返回值
注意! 接收完前五个字符, 记得加’\0’ 字符串结束标记, 否则usmart_strcmp无法比较
下一个while判断当前字符是不是’*’或者’(‘ , 判断逻辑是, 没有碰到空格或者 * 就继续循环, 同时res也就是offset加1
根据offset直接跳到函数名开始的地方
继续判断, 逻辑是 fover表示碰到的括号, 左括号加一, 右括号减一, 如果fover是0说明没有接收完, 因为至少有一个左括号或有右括号
判断是不是’,’ 是,temp加一(temp表示参数个数)

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
// 从str中得到函数名
//*str:源字符串指针
//*fname:获取到的函数名字指针
//*pnum:函数的参数个数
//*rval:是否需要显示返回值(0,不需要;1,需要)
// 返回值:0,成功;其他,错误代码
u8 usmart_get_fname(u8 *str, u8 *fname, u8 *pnum, u8 *rval)
{
u8 res;
u8 fover = 0; // 括号深度
u8 *strtemp;
u8 offset = 0;
u8 parmnum = 0;
u8 temp = 1;
u8 fpname[6]; // void+X+'/0'
u8 fplcnt = 0; // 第一个参数的长度计数器
u8 pcnt = 0; // 参数计数器
u8 nchar;
// 判断函数是否有返回值
strtemp = str;
while (*strtemp != '\0') // 没有结束
{
if (*strtemp != ' ' && (pcnt & 0X7F) < 5) // 最多记录5个字符
{
if (pcnt == 0)
pcnt |= 0X80; // 置位最高位,标记开始接收返回值类型 x000 0x00
if (((pcnt & 0x7f) == 4) && (*strtemp != '*')) // 0x7f ---> 0111 1111 和0x7f相与查看pcnt是不是等于4 且strtemp此时指向的不是 *
break; // 就跳出当前这一次循环, 因为最后一个字符,必须是*
fpname[pcnt & 0x7f] = *strtemp; // 记录函数的返回值类型 TODO: ?????
pcnt++;
}
else if (pcnt == 0X85)// 0x85 ---> 1000 0101
break;
strtemp++;
}
if (pcnt) // 接收完了
{
fpname[pcnt & 0x7f] = '\0'; // 加入结束符
if (usmart_strcmp(fpname, "void") == 0) // 看是不是void
*rval = 0; // 不需要返回值
else
*rval = 1; // 需要返回值
pcnt = 0;
}
res = 0;
strtemp = str;
while (*strtemp != '(' && *strtemp != '\0') // 此代码找到函数名的真正起始位置
{
strtemp++;
res++;
if (*strtemp == ' ' || *strtemp == '*')
{
nchar = usmart_search_nextc(strtemp); // 获取下一个字符
if (nchar != '(' && nchar != '*')
offset = res; // 跳过空格和*号
}
}
strtemp = str;
if (offset)
strtemp += offset + 1; // 跳到函数名开始的地方
res = 0;
nchar = 0; // 是否正在字符串里面的标志,0,不在字符串;1,在字符串;
while (1)
{
if (*strtemp == 0)
{
res = USMART_FUNCERR; // 函数错误
break;
}
else if (*strtemp == '(' && nchar == 0)
fover++; // 括号深度增加一级
else if (*strtemp == ')' && nchar == 0)
{
if (fover)
fover--;
else
res = USMART_FUNCERR; // 错误结束,没收到'('
if (fover == 0)
break; // 到末尾了,退出
}
else if (*strtemp == '"')
nchar = !nchar;

if (fover == 0) // 已经接受完了函数名了
{
if (*strtemp != ' ') // 空格不属于函数名
{
*fname = *strtemp; // 得到函数名
fname++;
}
}
else // 函数名还没接收完
{
if (*strtemp == ',')
{
temp = 1; // 使能增加一个参数
pcnt++;
}
else if (*strtemp != ' ' && *strtemp != '(')
{
if (pcnt == 0 && fplcnt < 5) // 当第一个参数来时,为了避免统计void类型的参数,必须做判断. void(超过5了,所以定为5
{
fpname[fplcnt] = *strtemp; // 记录参数特征
fplcnt++;
}
temp++; // 得到有效参数(非空格)
}
if (fover == 1 && temp == 2) // TODO:没懂???
{
temp++; // 防止重复增加
parmnum++; // 参数增加一个
}
}
strtemp++;
}
if (parmnum == 1) // 只有1个参数.
{
fpname[fplcnt] = '\0'; // 加入结束符
if (usmart_strcmp(fpname, "void") == 0)
parmnum = 0; // 参数为void,表示没有参数.
}
*pnum = parmnum; // 记录参数个数
*fname = '\0'; // 加入结束符
return res; // 返回执行结果
}

u8 usmart_strcmp(u8 *str1, u8 *str2)

比较两个字符串是不是一样, 和strcmp类似

1
2
3
4
5
6
7
8
9
10
11
12
13
u8 usmart_strcmp(u8 *str1, u8 *str2)
{
while (1)
{
if (*str1 != *str2)
return 1; // 不相等
if (*str1 == '\0')
break; // 对比完成了.
str1++;
str2++;
}
return 0; // 两个字符串相等
}

u8 usmart_search_nextc(u8 *str)

1
2
3
4
5
6
7
8
9
10
// 获取下一个字符(当中间有很多空格的时候,此函数直接忽略空格,找到空格之后的第一个字符)
// str:字符串指针
// 返回值:下一个字符
u8 usmart_search_nextc(u8 *str)
{
str++;
while (*str == ' ' && str != '\0')
str++;
return *str;
}

usmart_init—-初始化

初始化定时器, 使用的是定时器4, 主频也就是sysclk是72mhz, sysclk100-1是分频系数—> 72000000/(72 * 100-1 +1 )=10000hz, 所以一个定时器周期是1/10000==0.0001s==0.1ms, 定时器0.1ms1000====100ms中断一次

1
Timer4_Init(1000, (u32)sysclk * 100 - 1);

usmart_cmd_rec—-识别

usmart_cmd_rec函数逻辑
入口参数是从串口接收到的函数名字, 字符串指针
首先通过usmart_get_fname得到函数名字和参数个数
然后从0到usmart_dev.fnum(结构体里函数数量值)开始循环, strcmp逐个比较接收的函数名字和本地存储的函数名字
情况1. 如果找到一样的,判断收到的函数参数是不是和存储的函数参数个数相等
情况1.1 收到的小于存储的就返回错误USMART_PARMERR, 记下函数id, 跳出循环
情况1.2 收到的大于存储的, xxxxxxxxxxx
情况1.3 收到的等于存储的, 用usmart_get_fparam获得函数参数个数并赋值给usmart_dev.pnum
情况3. 如果i循环到等于usmart_dev.fnum, 也就是全部找完了, 没有发现符合的函数名字,也返回一个错误USMART_NOFUNCFIND
都判断完,找到了且参数个数也一样就返回USMART_OK

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
//入口参数是字符串指针
u8 usmart_cmd_rec(u8 *str)
{
u8 sta, i, rval; // 状态
u8 rpnum, spnum;
u8 rfname[MAX_FNAME_LEN]; // 暂存空间,用于存放接收到的函数名
u8 sfname[MAX_FNAME_LEN]; // 存放本地函数名
sta = usmart_get_fname(str, rfname, &rpnum, &rval); // 得到接收到的数据的函数名及参数个数
if (sta)
return sta; // 错误
for (i = 0; i < usmart_dev.fnum; i++)
{
sta = usmart_get_fname((u8 *)usmart_dev.funs[i].name, sfname, &spnum, &rval); // 得到本地函数名及参数个数
if (sta)
return sta; // 本地解析有误
if (usmart_strcmp(sfname, rfname) == 0) // 相等
{
if (spnum > rpnum)
return USMART_PARMERR; // 参数错误(输入参数比源函数参数少)
usmart_dev.id = i; // 记录函数ID.
break; // 跳出.
}
}
if (i == usmart_dev.fnum)
return USMART_NOFUNCFIND; // 未找到匹配的函数
sta = usmart_get_fparam(str, &i); // 得到函数参数个数
if (sta)
return sta; // 返回错误
usmart_dev.pnum = i; // 参数个数记录
return USMART_OK;
}
// 从str中得到函数名
//*str:源字符串指针
//*fname:获取到的函数名字指针
//*pnum:函数的参数个数
//*rval:是否需要显示返回值(0,不需要;1,需要)
// 返回值:0,成功;其他,错误代码
u8 usmart_get_fname(u8 *str, u8 *fname, u8 *pnum, u8 *rval)

usmart_exe—-执行

usmart_scan—-扫描

整体逻辑

  1. 初始化定时器四, 0.1ms产生一次中断, 执行usmart_scan进行扫描
  2. 扫描成功后执行usmart_cmd_rec识别接收的字符串
  3. 识别成功后执行usmart_exe运行函数

FSMC

FSMC—-灵活的静态存储控制器

能够与同步或异步存储器和16位PC存储器卡连接,STM32的FSMC接口支持包括 SRAMNANDFLASHNORFLASHPSRAM等存储器,支持8/16/32/位数据宽度。

fsmc框图

fsmc驱动LCD的原理——->nor存储控制器把TFTLCD当成一个SRAM来用,有两个地址的SRAM

fsmc存储块

!注意:

当Bank1接的是16位宽度存储器的时候:HADDR[25:1]->FSMC_A[24:0];

当Bank1接的是8位宽度存储器的时候:HADDR[25:0]->FSMC_A[25:0];

不论外部接8位/16位宽设备,FSMC_A[0]永远接在外部设备地址A[0]。

STM32F4仅写时序DATAST需要+1

SRAM模式A

模式A

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//LCD地址结构体
typedef struct
{
vu16 LCD_REG; // 0110 1100 0000 0000 0000 0111 1111 1110
vu16 LCD_RAM; //0110 1100 0000 0000 0000 1000 0000 0000 16进制+1--->8进制加2
} LCD_TypeDef;

//使用NOR/SRAM的 Bank1.sector4,地址位HADDR[27,26]=11 A10作为数据命令区分线
#define LCD_BASE ((u32)(0x6C000000 | 0x000007FE))
//注意当Bank1接的是16位宽度存储器的时候:HADDR[25:1] ---> FSMC_A[24:0]
//右移一位对齐,对应到地址引脚即A10:A0 --- > 0011 1111 1110 A10是 0
//0x6C000000 ---> 0110 1100 0000 0000 0000 0000 0000 0000
//0x000007FE ---> 0000 0000 0000 0000 0000 0111 1111 1110
//相与的结果
// 0110 1100 0000 0000 0000 0111 1111 1110
#define LCD ((LCD_TypeDef *) LCD_BASE)
1
2
3
4
5
6
LCD_BASE,根据外部电路的连接来确定,如Bank1.sector4就是从地址0X6C000000开始,而0X000007FE,则是A10的偏移量
7FE ---> 0111 1111 1110
16位数据,地址右移一位对齐,对应到地址引脚 ---> 0011 1111 1111
#define LCD ((LCD_TypeDef *) LCD_BASE)强制转换后,LCD_REG---> 0110 1100 0000 0000 0000 0111 1111 1110
对应到A10是016位+18位加2 得到0110 1100 0000 0000 0000 1000 0000 0000 A10是1
从而实现对RS的控制