2023年10月

操作系统 :CentOS 7.6_x64

FreeSWITCH版本 :1.10.9

之前写过FreeSWITCH添加自定义endpoint的文章,今天整理下api及app开发的笔记。历史文章可参考如下链接:

FreeSWITCH添加自定义endpoint
FreeSWITCH添加自定义endpoint之媒体交互

一、常用函数介绍

这里列举下开发过程中常用的函数。

1、根据uuid查询session

使用switch_core_session_locate宏进行查询。

定义如下:

#define switch_core_session_locate(uuid_str) switch_core_session_perform_locate(uuid_str, FILE, SWITCH_FUNC, LINE)

示例如下:

switch_core_session_t *session;if ((session =switch_core_session_locate(uuid))) {
switch_channel_t
*tchannel =switch_core_session_get_channel(session);
val
=switch_channel_get_variable(tchannel, varname);
switch_core_session_rwunlock(session);
}

查询session后,需要使用switch_core_session_rwunlock函数释放锁。

2、获取session的uuid

使用 switch_core_session_get_uuid 函数根据session查询uuid。定义如下:

SWITCH_DECLARE(char *) switch_core_session_get_uuid(_In_ switch_core_session_t *session);

示例如下:

const char *uuid = switch_core_session_get_uuid(session);

3、根据session获取channel


使用 switch_core_session_get_channel 函数根据session查询channel。定义如下:
_Ret_ SWITCH_DECLARE(switch_channel_t *) switch_core_session_get_channel(_In_ switch_core_session_t *session);

示例如下:

switch_channel_t *tchannel = switch_core_session_get_channel(session);

4、channel操作

  • switch_channel_set_name

设置通道名称,通常以 endpoint 类型作为前缀,比如"sofia/1001"、"rtc/1002"等。

  • switch_channel_get_name

获取通道名称。

  • switch_channel_set_variable

设置通道变量的值。

  • switch_channel_get_variable

获取通道变量的值。

  • switch_channel_set_flag

设置channel的标记

  • switch_channel_ready

判断channel是否就绪

  • switch_channel_set_caller_profile

设置profile属性


更多内容channel操作可参考 switch_channel.h 文件。

二、查看已有api及app

使用 show modules 显示所有api、app及mod对应关系。

效果如下:

如需查看单个模块包含的api及app,可以在后面加上模块名称,比如:

show modules mod_db

三、新增api命令

通过SWITCH_STANDARD_API进行添加。

比如添加如下命令:

ctest_update_token <uuid> <token>

示例代码如下:

/*<uuid> <token>*/SWITCH_STANDARD_API(ctest_update_token_function)
{
char *argv[3];char *mydata;char *uuid, *token;if(zstr(cmd)) {
stream
->write_function(stream, "-ERR Parameter missing\n");returnSWITCH_STATUS_SUCCESS;
}
if (!(mydata =strdup(cmd))) {returnSWITCH_STATUS_FALSE;
}
if (!switch_separate_string(mydata, ' ', argv, (sizeof(argv) / sizeof(argv[0]))) || !argv[0]) {gotoend;
}

uuid
= argv[0];
token
= argv[1];if (zstr(uuid) ||zstr(token)) {gotoend;
}

switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO,
"uuid : %s , token : %s\n",uuid,token);

end:
switch_safe_free(mydata);
returnSWITCH_STATUS_SUCCESS;
}

在模块加载函数(mod_ctest_load)添加入口:

switch_api_interface_t *api_interface;

SWITCH_ADD_API(api_interface,
"ctest_update_token", "update ctest channel token", ctest_update_token_function,"<uuid> <token>");

设置自动填充uuid:

switch_console_set_complete("add ctest_update_token ::console::list_uuid");

运行效果如下:

四、新增app命令

通过 SWITCH_STANDARD_APP 添加,这里就不详细描述了,具体看下 echo 这个app:

mod/applications/mod_dptools/mod_dptools.c :2317SWITCH_STANDARD_APP(echo_function)
{
switch_ivr_session_echo(session, NULL);
}

好,就这么多了,希望对你有帮助。

本章节主要围绕着手撕 Vuex,那么在手撕之前,先来回顾一下 Vuex 的基本使用。

创建一个 Vuex 项目,我这里采用 vue-cli 创建一个项目,然后安装 Vuex。

vue create vuex-demo

image-20231026224666666021

选择 Manually select features。

image-20231026225142303

这里只需要,Babel 与 Vuex。

选择 2.X 版本的 Vue:

image-20231026225824095

创建 package.json:

image-20231026225838388

是否保存为模板这里我选择不:

image-20231026225904183

到这里我们的模板项目就创建完毕了。

紧接着找到
store
文件夹下的 index.js,在文件中导入了 Vuex, 可以看到 Vuex 的使用非常简单,只需要创建一个 store,然后在 Vue 的实例中注入 store,就可以在组件中使用了。

在基础的结构当中分别有 state、mutations、actions、modules 每个属性分别作用如下:

  • state:存放状态,可以通过 this.$store.state 访问到。(用于保存全局共享数据)
  • mutations:存放同步修改 state 的方法,可以通过 this.$store.commit 方法访问到。(用于同步修改共享数据)
  • actions:存放异步修改 state 的方法,可以通过 this.$store.dispatch 方法访问到。(用于异步修改共享数据)
  • modules:存放模块,可以通过 this.$store.state.模块名 访问到。(用户模块化共享数据)

在 Vuex 当中定义的数据,都是全局共享的,所以在任何一个组件当中都可以通过 this.$store.state 访问到,接下来先简单的使用一下 Vuex。

在 state 当中定义一个 name,然后在组件当中使用。

App.vue:

<template>
  <div id="app">
    {{ this.$store.state.name }}
    <HelloWorld/>
  </div>
</template>

HelloWorld.vue:

<template>
  <div class="hello">
    {{ this.$store.state.name }}
  </div>
</template>

npm run serve 启动项目,可以看到页面上显示了 name 的值。

接下来就可以开始手撕 Vuex。在手撕之前我们先分析一下 Vuex 的使用过程。

在 index.js 首先是将 Vuex 导入了进来,然后通过 Vue.use(Vuex) 将 Vuex 注册到 Vue 的原型上,Vue.use(Vuex) 这一步其实是对 Vue 的扩展就是安装了 Vuex 插件,这就是 Vuex 的第一个特点。

Vuex 特点1

  • 使用 Vuex 的时候需要用到 Vue 的 use 方法

那么在之前的 Vue 文章介绍到 use 方法是用于注册插件的,所以 Vuex 的本质就是一个插件。所以实现 Vuex 就是在实现一个全局共享数据的插件。

Vuex 特点2

在使用 Vuex 的时候我们会通过 Vuex.Store 创建一个仓库,所以还需要在 Vuex 中新增 Store 属性,这个属性的取值是一个类。

Vuex 特点3

为了保证每个Vue实例中都能通过
this.$store
拿到仓库, 我们还需要给每个Vue实例都动态添加一个
$store
属性

?> 小 Tips:博主建议在继续往下看之前,先去看看 Vue 插件的开发,这样会对 Vuex 的开发有更深的理解。

这节课中介绍了训练神经网络的第一部分,包括激活函数的选择,权重初始化,数据预处理以及正则化方法

激活函数

这部分主要讨论我们之前提到的几种激活函数的利弊:
img
首先我们看sigmoid函数,这种激活函数有着激活函数中常见的优点与缺点:
img
优点方面,它可以使数据分布在0-1之间,可以很好地表示神经元的饱和放电现象
缺点方面
我们考虑使用这个激活函数进行梯度的反向传播:
img
我们可以看到在x = 10或者x = -10时,传播的梯度都会接近于0,导致后面的所有梯度均变为0,这就会导致梯度消失,我们的神经网络无法学习
同时,sigmoid激活函数的输出并不是以0为中心:
img
我们知道w梯度的反向传播的值于x相关,所以当x一直为正时,w梯度将取决于w的正负,所以梯度很可能会在正负之间横跳,这样也不利于神经网络的学习,但是在小数据集上还是可以使用,并且小哥指出这一点其实不是特别关键
最后,就是exp()这种非线性操作从底层来说很花费时间(相对于线性操作来说)
之后我们再来看看其它激活函数:
tanh它的优缺点与sigmoid函数大致相同:
img
然后是最常用的relu激活函数:
img
relu函数有很多优异的性质,比如不会导致饱和杀死梯度,计算开销低速度快,神经网络收敛的也快
但是还有一个人们一直担心的问题就是:
img
小于0的数据永远不会被激活无法学习
因此人们又想出来了很多relu的变体:
leaky relu在小于0的区域加入一个线性函数,更进一步地,可以把小于0区域函数的系数作为一个可学习的参数加入神经网络,通过反向传播来更新优化
img
elu 相对于leaky relu添加了鲁棒性,但是计算开销增大
img
selu 对深度神经网络的效果很好 甚至可以不用batch normalization就可以训练深层神经网络 而其中的参数是固定的,是通过91页的数学推导得出的结果(震惊小哥一万年)
img
最后再放上不同激活函数数据的统计以及小哥的建议:
img
img

数据预处理

数据的预处理最常见的就是从均值与标准差入手:
img
当然也有少数使用PCA降维与数据白化:
img
它们的基本思想都是减少模型的敏感性,使其更容易优化收敛:
img
举个例子,Alexnet VGG ResNet都采用了不同的数据预处理方法,Alexnet减去整个图像的均值,VGG减去每个通道的均值,而Resnet减去每个通道的均值再除以每个通道的标准差
img

权重初始化

在训练神经网络时,我们面临的问题就是如何进行权重矩阵的初始化
首先一个基础的想法就是都初始化为0矩阵,但是这样会导致所有的输出都是0,梯度都一样,无法学习
其次我们会想全0不行,那随机生成一些随机数应该可以了吧,我们可以使用python中randn函数,利用高斯分布生成随机数,但是这样的方法只使用于小的神经网络,不适用于更深的神经网络,为什么呢?
img
我们把在不同层的数据分布图画出来,可以看到:
img

img
std系数小的时候,数据在深层会集中在0,std大的时候就会出现之前的过饱和现象,杀死梯度,这两种情况都会导致神经网络无法学习

针对上述问题,人们想出来了Xavier initialization,把之前的std改为除以输入维度开根号,这是针对全连接层,卷积层是卷积核的平方乘以输入维度再开根号,我们可以看到效果很好,基本维持住了数据的形状
img
这里的原理主要就是我们想要使输入方差等于输出方差,这一点可以利用基本的概率论知识进行推导,建立在相互独立以及均值为0的假设上:
img
但是上述神经网络中我们采用的是tanh激活函数,假如我们使用relu激活函数,那么均值为0的假设就不成立,这时候人们推导出针对relu的Kaiming initialization:
img

上面提到了全连接层还有卷积层,还没有提到残差网络,针对残差网络比较特殊,如果我们使用上述初始化,使得输入与输出的方差相等的话,由于残差块还要加上一个x,所以方差是一定会增大的,这里我们可以把第一个卷积层采用kaiming initialization初始化,第二个卷积层直接初始化为0,这样我们残差块的输入与输出的方差就相等了:
img

正则化

正则化方法最常用的就是我们直接衡量矩阵的L2distance等等:
img

也有我们之前提到的dropout方法:
img
这里的思想是神经网络中可能有冗余的特征,通过dropout我们可以放弃这些冗余的特征,降低特征与特征之间的关联,这样有利于我们提取到更重要更有效的特征:
img

这里引入正则化的一般模式,思想是在训练的时候我们加入随机性,在测试的时候我们平均这种随机性防止混乱:
img

dropout的实现也是如此:
img
注意dropout只使用于全连接层,对于放弃了全连接层的网络一般使用batch normalization:
img

既然提到了正则化的思想是增加随机性,我们可以在输入数据上做更多的处理,采用不同的数据增强方法:
我们可以旋转我们的图像:
img
将图像划分为不同的区域:
img
甚至改变颜色:
img
总之我们可以做任何疯狂的事情来帮助我们的神经网络学习不同复杂的情况:
img

除了数据增加,我们还有dropconnect,随机放弃神经元之间的连接:
img
选择随机的池化区域:
img
随机跳过一些残差块:
img
随机将图像的一部分像素设为0(cutout):
img
将不同的图像混合训练:
img
最后是小哥的建议!
img

定时控制接口

8253/8254定时器

由数字电路中的计数电路构成用于记录输入脉冲的个数,常用于定时中断、定时检测

定时器的应用

并行接口

并行接口电路8255

内部引脚

image

  • 8225具有24条可编程输入输出引脚,分成3个端口:
    A

    B

    C
    端口每个端口都是8位
  • A、B、C、3个端口分成两组进行控制,
    A组
    控制端口A和端口C的高部(PC7
    PC4),`B组`控制B端口和C端口的低半部(PC3
    PC0)
  • 端口A和端口B作为输入和输出的数据端口,端口C作为控制或状态端口

工作方式

工作方式0:基本输入输出方式

工作方式0是一种基本的输入输出方式,不需要应答式的联络信号

  • D0~D7
    :8255与处理器间的数据引脚
  • 端口
    :是指8255与外设间的数据引脚
    PA0~PA7

    PB0~PB7
    、和
    PC0~PC7

当处理器执行IN指令时,产生读信号
~RD
,控制8255从端口读取外设的输入数据,然后从
D0~D7
输入处理器

当处理器执行输出OUT指令时,产生写信号
~WR
,将处理器的数据从
D0~D7
提供给8255,然后控制8255将该数据从端口提供给外设

8255的三个端口都可以工作在基本输入输出方式,

工作方式1:选通输入输出方式

借助于选通(应答)联络信号进行输入或输出的工作方式。8225只有
端口A和端口B
可以采用方式1,但是每个数据端口要利用端口C的3个数据引脚作为应答联络信号。
当8225的
端口A

端口B
都工作于方式1,此时端口C剩下的两个引脚还可作为基本输入输出引脚
image

  • (~STB)选通信号
    :这是由外设提供的输入信号,有效时,将输入设备送来的数据锁存至8255的输入锁存器
  • IBF
    :输入缓冲满信号,8255的联络信号,当此信号有效时,表示数据已锁存至输出锁存器
  • INTR
    :中断请求信号,用于向处理器发出中断请求,要求处理器读取外设数据

image

选通输出方式
image

编程

8225是通用并行接口,在具体应用时,要根据实际情况选择工作方式,也就是需要进行输出设置。
8255的初始化编程只需要一个方式控制字就可以把三个端口设置完成

  1. 写入方式控制字
    image

例如我们要把A端口指定为方式1输出,C端口上半部分指定为输出,B端口指定为方式0输出,C端口下半部分指定为输入,方式控制字:0B1H,或10110001B

mov dx, 0fffeh		;假设控制端口的地址为FFFEH
mov ak,0b1h			;方式控制字
out ax,al			;送到控制端口
  1. 读写数据端口
    经过了步骤1对读写方式的初始化之后,处理器执行IN指令、OUT指令,对3个数据端口进行读写就可以实现处理器与外设之间的数据交换
    如何实现对数据端口的置位:
mov dx,0fffah		;B端口地址假设位FFFAH
in al,dx			;读出B端口原输出内容
or al, 80h			;使D7=PB7=1
out dx,al			;输出新的内容
  1. 读写端口C
  • C端口主要被分为两个4位端口,两个端口只能以0方式工作,但可分别选择输入和输出(但情况必须合法比如用火选择PC0为输入、PC1为输出将无法办到,因为他们两个都是属于C端口的下半部分的只能同输入输出)
  • 当端口A和B工作在方式1下,C端口的部分引脚将被征用,其余引脚可设定工作在方式0
  • 对端口C的数据输出有两种办法
    • 通过端口C的I/O地址,向C端口直接写入字节数据。
    • 通过控制端口,向C端口写入位控制字,使C端口的某个引脚输出0或1,或置位、复位内部的中断允许触发器
      image
  • 读取的C端口数据有两种情况
    • 对未被A端口和B端口征用的引脚,将从定义为输入的端口读到引脚输入信息,将从定义为输出的端口读到输出锁存器的信息。
    • 对被A、B端口征用作为联络线的引脚,将读到反映8255状态的状态字
      image

并行接口的应用

例子:
image

mov al,10011001b		;8255的方式控制字我9H
out 63h,al				;设置端口A和端口C为方式0输入,端口B为方式0输出

用8255方式0与打印机接口

image

采用端口C的PC7引脚产生负脉冲选通信号,PC2引脚连接打印机的忙信号查询其状态,处理器利用擦查询方式输出数据。

;初始化程序段
mov dx,0fffeh		;控制端口地址为FFFEH
mov al,10000001B	;方式控制字
out dx,al			;A端口方式0输出C端口上半部分输出、下半部分输入(端口B任意)
mov al,00006666661B	;端口C的复位/置位控制字
out dx,al			;使PC7=1,即置~STORBE=1

;输出打印数据子程序,入口参数AH=打印数据
printc	prc
		push ax
		push dx
prn:	mov dx,0fffch	;读取端口C
		in al,dx		;查询打印机的状态
		and al,04h		;打印机忙否
		jnz prn			;PC2=1打印机忙,则循环等待
		mov dx,0fff8h	;PC2=0打印机不忙,则输出数据
		mov al,ah
		out dx,al		;将打印数据从端口A输出
		mov dx, 0fffeh	;从端口C的PC7送出控制低脉冲
		mov al,00006666660B;使PC7=0,即置~STORBE=0
		out dx,al
		nop				;适当延时,产生一定宽度的低电平
		nop
		mov al,00006666661B;使PC7=1,置~STROBE=1
		out dx,al
		pop dx
		pop ax
		ret
printc	endp

数码管及其接口

数码管的工作原理

LED数码管的主要部分是7段发光管,这七段发光管顺时针分别被称为a、b、c、d、e、f、g,有的还有一个小数点h。通过7个发光段的不同组合,数码管可以显示0
9和A
F共16个字母数字,从而实现十六进制数的显示。
image
LED数码管有共阳极和共阴极两种结构。如果共阳极结构。
共阳极的话就是低电平有效
共阴极的话就是高电平有效

单个数码管的显示

为了将一位十六进制数在一个LED数码管上显示出来,就需要将一位十六进制数译为LED的7位显示代码,一种方法是硬件方法,另一种方法是软件方法,软件方法就是将0~F这16个数字对应的显示代码组成一个表,通过查表进行译码

ledtb byte 3f,06h,5bh,...,显示数码表
mov bx,1	;要显示的数字
mov al,ledtb[bx];
mov dx,port	;假设port表似乎与数码管相接的端口地址
out dx,al	;输出显示

多个数码管的显示

多个数码管进行显示的话我们需要考虑两个问题,就是选中那个数码管,然后让这个数码管显示什么数字,首先我们已经把第二个问题(显示什么数字)解决了(单个数码管的显示),然后选中那个数码管可以用一个位控制端口

  • 位控制端口
    :控制哪个数码管显示
  • 段控制端口
    :决定具体显示什么数码

image

	;数据段
ledt	byte 8 dup(0)
	;主程序
	mov si,offset ledt	;指向数码缓冲区
	call displed
	;子程序:显示一次数码缓冲区的8个数码,入口参数:DS:SI=缓冲区首地址
displed	proc
	push ax
	push bx
	push dx
	xor bx,bx
	mov ah,0feh			;指向最左边的数码管
led1:	
	mov bl,[si]			;取出要显示的数字
	inc si
	mov al,ledtb[bx]	;得到显示代码:AL<-LEDTB[BX]
	mov dx,segport		;segport位段控制端口
	out dx,al			;送出段码
	mov al,ah;			取出位码
	mov dx,bitport		;bitport为位控制端口
	out dx,al			;送出位码
	call delay			;实现数码管的延时显示
	rol ah,1			;指向下一个数码管
	cmp ah,0feh			;是否指向最右边的数码管
	jnz	led1			;没有显示下一个数字
	pop dx
	pop bx
	pop ax
	ret					;8位数码管都显示一遍,返回
ledtb	byte	0c0h,0f9h,0a4h,0b0h,99h,92h,82h,0f8h
		byte	80h,90h,88h,83h,0c6h,0c1h,86h,8eh
displed	endp
timer	=10				;延时常量(需要根据实际情况确定具体数值)
delay	pro
...
delay	endp

异步串行通讯接口

模拟接口

在现实中许多信号并不是数字信号,而是计算机无法处理的模拟信号,模拟接口就是为了处理这类信号。

模拟输入输出系统

image

  • 传感器
    :将各种现场的物理量测量出来并转换成电信号
  • 放大器
    : 把传感器输出的信号放大到ADC所需的量程范围
  • 低通滤波器
    :降低噪声、滤去高频干扰
  • 多路开关
    :把多个现场信号分时地接通到A/D转换器上转换
  • 采样保持器
    :对高速变化的信号进行A/D转换时,为了保持转换精度,需要使用采样保持器,周期性采样连续信号,并在A/D转换期间保持不变

D/A转换器

D/A转换器(DAC)将微机处理后的数字量转换为模拟量

D/A转换原理

将数字量转换为模拟量,先把每一位代码按其权值的大小转换成相应的模拟分量,然后将各模拟分量相加,总和就是与数字量相应的模拟量。例如1101B=13
Vref:基准电压,提供给转换电路的稳定的电压源,也称参考电压Vref

DAC0832的数字接口

image
DAC0832内部具有输入寄存器和DAC寄存器两级数字量缓冲寄存器
DI0~DI7
是8为数字量输入引脚
ILE

~CS

~WR1
控制输入寄存器锁存信号
LE1
~XFER

WR2
:控制DAC寄存器的锁存信号
LE2
三种工作方式

  • 直通方式
    :LE1和LE2一直为高,数据可以直接进入D/A转换器
  • 单缓冲方式
    :LE1或LE2一直为高,只控制一级缓冲
  • 双缓冲方式
    :不让LE1和LE2一直为高,控制两级寄存器

模拟输出

DAC0832的模拟输出有
Iout1

Iout2

Rfb

  • Iout1
    :模拟输出电流1,是逻辑电平为1的各位输出电流之和,输出全为1的时候值最大,输出全为0的时候值最小
  • Iout2
    :模拟输出电流2,是逻辑电平为0的各位输出电流之和,Iout1 + Iout2 = 常量
  • Rfb
    :反馈电阻引出端,用作外接运算放大器的反馈电阻
  • VREF
    :参考电压输出端
  • Vcc
    :电源电压
  • AGND
    :模拟地
  • DGND
    :数字地
    image

DAC芯片与主机的连接

DAC芯片作为一个输出设备接口电路,与主机相连时中间需要有锁存器

mov al,buf	;取数字量
mov dx,portd	;PORTD为DAC端口地址
out dx,al		;输出,进行D/A转换

A/D转换器

A/D转换器将模拟量转换为数字两输入微机处理

A/D转换原理简介

ADC主要有两种实现技术,一种是计数器式,一种是逐次逼近式。
计数式的原理:就是从0开始加,产生数字量,然后用D/A转换器,将数字量转换成模拟量,和输入进来的模拟量进行比较,看是否相同,不相同的话继续累加,遇到相同的则说明此时寄存器里面存储的就是该模拟信号要转换的数字量
逐次逼近式:是从最大的数开始减,类似计数式

ADC0809的模拟输入

image
ADC8089提供8通道的多路开关和寻址逻辑

  • IN0~IN7
    :8个模拟电压输入端
  • ADDA、ADDB、ADDC
    :3个地址输入线,经过译码器后选择一个模拟输入端进行A/D转换

ADC0809的数字输出

ADC0809内部对转换后的数字量具有锁存能力,数字量输出端D0
D7具有三态功能,只有在OE为高电平有效时,才将三态锁存器的梭子两从D0
D7输出
8为A/D转换器,从输入模拟量Vin转换为数字模拟量N的公式:
image

ADC芯片和主机连接

ADC芯片是一个向主机输出的设备。
ADC开始转换时需要一个启动信号。
ADC转换结束时,ADC输出一个转换结束信号,通知主机读取结果,主机检查判断A/D转换结束的方式主要有两种一种是查询方式、一种是中断方式

  • 查询方式
    :ADC开始转换后,主机不断查询数据总线上的某一位(这一位是ADC在转换结束后发送到数据总线上的),发现结束信号有效时,才读取数据
  • 中断方式
    :把结束信号作为中断请求信号接到主机的中断请求线上。ADC转换结束后,主动向处理器申请中断。处理器响应中断后,在中断服务程序中读取数据。

ADC芯片的应用:中断方式

image

;.data设置缓冲区
adtemp byte 0
;.code
...		;设置中断向量表等工作
sti		;开中断
mov dx,220h
out ad,al		;启动A/D转换
... ;其他工作

中断服务程序

adint	proc		;中断服务程序
		sti			;开中断
		push ax		;保护寄存器
		push dx
		push ds
		mov ax,@data	设置数据段DS的段地址
		mov ds,ax
		mov dx,220h
		in al,dx	;读取A/D转换后的数字量
		mov adtemp,al	;送入缓冲区
		mov al, 20h	;给中断控制器发送EOI命令
		out 20h,al
		pop ds
		pop dx
		pop ax
		iret		;中断返回
adint	endp

ADC芯片的应用:查询方式

image

	;.data
counter	equ 8
buf		byte	counter dup(0)
	;.code
	mov bx, offset buf;数据缓冲区偏移地址
	mov cx, counter;检测的数据个数
	mov dx,220h	;从IN0开始转换
start1:	
	out dx,al	;启动A/D转换
	push dx
	mov dx, 238h	;循环查询是否转换结束
start2:
	in al, dx	;读入状态信息
	test al, 80h	;D7=1转换结束
	jz start2	;没有结束继续查询
	pop dx		;转换结束
	in al,dx	;读取数据
	mov [bx],al	;存入缓存区
	inc cx
	inc dx		;转向下一个模拟通道进行检测
loop start1
...	数据处理

在进行2D游戏开发时,跳跃是不可缺少的一个重要功能。但是我们在Unity开发时Unity本身的物理引擎并不能提供很好的的手感,下落的时候轻飘飘的,这操作起来显然非常不舒服。所以,我们需要自己对跳跃进行优化,以此来获得更好的手感。我们不难发现,在绝大多数2D游戏的跳跃中,下落的速度比上升的速度要快上很多,下落的用时是上升的一半左右。

提示,在优化跳跃时,将使用刚体组件

首先,我们需要对Unity物体跳跃阶段进行分析:

图中,跳跃共分为两个阶段,第一阶段就是跳起,人物上升阶段,第二阶段就是下落阶段

在第一阶段中,刚体向上速度为正,即velocity.y>0;第二阶段下落,物体的velocity.y<0,所以我们可以通过velocity.y的正负来确定物体所处哪一个阶段,进而对物体可以有更精确的操作。

接下来,我们就要对物体跳跃阶段进行操作,首先我们需要判定物体受否在面上,并且按下了W或其它跳跃按键,触发跳跃后,我们给物体的velocity.y赋一个初始值V,代表初始速度,那么最终跳跃高度的变化为h=Vt-1/2gt
2(g为重力加速度,t为时间),物体速度velocity.y=V-gt,当velocity.y=0时,物体到达最高点,此时物体高度(最高点)H=(V
2)/(2g)。

接下来,物体仍然受到重力加速度的作用,物体速度velocity.y的值继续减小,变为了负值,而这也标志着物体开始下落。从图中可以看出当物体只具有一个向上的初速度并且只受重力时,它的上升时间和下落时间是一样的,T=V/g。所以,我们为了确保下落用时是上升的一半,所以我们需要改变物体的重力加速度,使它变为原来的二倍,即修改Physics2D.gravity的值,使它的y的值乘2。

这样我们就完成了一个跳跃的优化。代码如下:

//提示!本代码使用射线检测物体是否位于地面
//提示!各变量含义
//rb                   刚体组件
//jumpSpeed            跳跃初始速度
//fullSpeed            下落重力加速度倍数
//isGround             玩家是否在地面
//rayDistance          射线距离
//groundLayerMask      地面的图层
void Update()
    {
        Ground();
        if (Input.GetKeyDown(KeyCode.W) && isGround)
        {
            Jump();
        }
        if (rb.velocity.y <= 0)
        {
            Full();
        }
    }
    private void Jump()
    {
        rb.velocity = Vector2.up * jumpSpeed;
    }
    private void Full()
    {
        //重力加速度变为fullSpeed倍
        Physics2D.gravity = new Vector2(0, -9.8f * fullSpeed);
    }
    //射线检测,是否在地面
    private void Ground()
    {
        // 从物体下方投射一条射线,看它是否与地面碰撞
        RaycastHit2D hit=
        Physics2D.Raycast(transform.position,Vector2.down,rayDistance,groundLayerMask);
        if (hit.collider != null)
        {
            isGround = true;
        }
        else
        {
            isGround = false;
        }
    }