0%

ANSI C标准

ANSI C是美国国家标准协会(ANSI)对C语言发布的标准。使用C的软件开发者被鼓励遵循ANSI C文档的要求,因为它鼓励使用跨平台的代码。

发展过程中产生了C89、C90、C99、C11四套标准,最早的C89在1983年创立,C90是1990年创立的ANSI C标准(带有一些小改动),C99在2000年3月创立,C11在2011年12月创立。

C预处理器

指令 描述
#define 定义宏
#include 包含一个源代码文件
#undef 取消已定义的宏
#ifdef 如果宏已经定义,则返回真
#ifndef 如果宏没有定义,则返回真
#if 如果给定条件为真,则编译下面代码
#else #if 的替代方案
#elif 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码
#endif 结束一个 #if……#else 条件编译块
#error 当遇到标准错误时,输出错误消息
#pragma 使用标准化方法,向编译器发布特殊的命令到编译器中

预定义宏

ANSI C 定义了许多宏。在编程中可以使用这些宏,但是不能直接修改这些预定义的宏。

描述
__DATE__ 当前日期,一个以 “MMM DD YYYY” 格式表示的字符常量。
__TIME__ 当前时间,一个以 “HH:MM:SS” 格式表示的字符常量。
__FILE__ 这会包含当前文件名,一个字符串常量。
__LINE__ 这会包含当前行号,一个十进制常量。
__STDC__ 当编译器以 ANSI 标准编译时,则定义为 1。

#pragma

#pragma指令的作用是:用于指定计算机或操作系统特定的编译器功能。C 和 C++ 的每个实现均支持某些对其主机或操作系统唯一的功能。 例如,某些程序必须对将数据放入的内存区域进行准确的控制或控制某些函数接收参数的方式。 在保留与 C 和 C++ 语言的总体兼容性的同时,#pragma 指令使每个编译器均能够提供特定于计算机和操作系统的功能。

根据定义,#pragma指令是计算机或操作系统特定的,并且通常对于每个编译器而言都有所不同。 #pragma指令可用于条件语句以提供新的预处理器功能,或为编译器提供实现所定义的信息。

#pragma pack([ show ] | [ push | pop ] [, identifier ] , n),用于内存对齐。
#pragma message(messageString),用于不中断编译的情况下,发送一个字符串文字量到标准输出。

编译器可识别的指令还有很多。

##和…

## 起连接字符串的作用,用于把参数宏中的“形参”与其它没有天然分割的内容粘连在一起。
例如:#define def_u32_array(__name, __size) uint32_t array_##__name[__size];
def_u32_array(sample_buffer, 64),宏展开后为:uint32_t array_sample_buffer[64];

... 是ANSI-C99标准引入的另外一个参数宏扩展,“可变参数宏”,其实就是将__VA_ARGS__替换为…中的值。
例如:#define log_info(__STRING, ...) printf(__STRING, __VA_ARGS__)
log_info("Count:%d", total_cycle_cnt);,宏展开后:printf("Count:%d", total_cycle_cnt);
log_info("-----------------\r\n");,宏展开后:printf("-----------------\r\n",);,…无参数,__VA_ARGS__为空,但是宏展开后仍有逗号。

逗号后无内容可能会产生个warning,想解决逗号问题,可以##和…一起使用。
例如:\#define log_info(__STRING, ...) printf(__STRING,##__VA_ARGS__)
log_info("-----------------\r\n");,宏展开后:printf("-----------------\r\n");

…初始化数组

int a[5] = {[0...2] = 1, [3...4] = 2};使数组a[0]~a[2] = 1, a[3]和a[4] = 2。

这种写法只可以在gcc编译C的情况下使用,gcc编译c++也不行。

绪论

研究内容

本文介绍了如何设计一个以Altera FPGA为核心的网络摄像机,将图像传感器和MEMS麦克风作为的主要传感器,辅以千兆以太网模块、电源模块等模块,最终完成实现一个低成本易使用的网络摄像机的功能。要求该网络摄像机能够实时传输图像和音频信息,在电脑端可以将图像实时处理显示并将音频实时播放。此外,还能够将数据流存储在设备中,便于用户对视频进行回放等操作。

总体设计方案描述

本设计方案是以Altera FPGA为核心,在Quartus 18.0开发平台上,实现FPGA接收图像传感器和麦克风数据,并通过千兆以太网PHY芯片,将图像和声音数据发送至电脑上位机,电脑上位机利用Python3编程语言编写,在PyCharm IDE中执行,实现音视频实时播放和音视频回放的功能。主要由Altera FPGA控制器、图像传感器模块、麦克风模块、以太网模块和电脑上位机组成,系统框图如下图所示。

220501-毕业设计-1.png

硬件选型

FPGA选用Cyclone IV EP4CE15 Starter Kit开发板。开发板板载W25Q64 SPI Flash芯片,8MB字节的存储容量,开发板给主控提供了50MHz的外部时钟源,芯片逻辑单元数为 15K LE,开发板引出了芯片的JTAG调试端口,引出了GMII千兆以太网接口,采用了RealTek的RTL8211EG芯片,引出了一个CMOS/CCD摄像头接口和40P的排座。开发板条件满足本设计的所有需求。

图像传感器选择经典的OV2640模组,OV2640适配开发板引出的40P的排座,提供了SCCB接口,可供配置传感器参数,可以配置最大1600*1200分辨率的图像输出,还可以配置JPEG压缩格式输出图像,模组拥有8位并口,可以高速输出图像信息,拥有帧场同步信号管脚,和电源使能管脚,方便FPGA控制和接收图像信息。

麦克风选择INMP441全向麦克风传感器,INMP441麦克风直接输出数字信号,采用的接口是I2S,非常适合FPGA去读取总线数据。传统驻极体麦克风输出模拟信号,选用INMP441省去了传统驻极体麦克所需的放大器、滤波器和模数转换器等硬件设计。

FPGA设计方案

FPGA设计采用模块化设计的方案,通过模块化设计,可以使一个大型设计分成多个模块,这样分工协作可使仿真测试更加容易,并且代码维护和代码升级也会更加方便。顶层模块不做逻辑设计,只通过例化调用子模块接口。因此,顶层模块下就由各功能模块组成,各功能模块下还可以分成多个子功能模块来实现。通过编译器对每个模块的综合仿真约束等设计,最后将所有模块连在一起,构成整个网络摄像机FPGA部分的设计。

本设计模块层次设计图如下所示,由时钟模块、数据交互模块、图像传感器模块、麦克风模块、千兆网络模块总共5个模块组成,各模块又由各个子模块组成,顶层模块通过例化的方式,将5个模块之间的接口互相连接起来,实现顶层模块的最终设计。该模块层次设计的各个子模块功能相对独立,各模块内部联系紧密,模块之间的连接简单,满足FPGA的模块化设计基本规则。

220501-毕业设计-2.png

上位机设计方案

上位机采用Python3编程语言设计,主要功能分为接收数据包、实时解码播放、保存回放。FPGA传来的UDP数据包包含了音频和图像,两者通过两个不同的端口传输,这样更便于应用层的分开处理。在UDP数据包中还加入了帧序列号,上位机可通过校验前后两帧的序列号是否对应,来判断是否发生掉包的现象。实时解码播放是通过数据包的传输协议将有用数据部分取出,图像数据为JPEG压缩图像,通过OpenCV对JPEG图像解码,通过图像流的方式显示在屏幕上。音频是通过PCM码流传输,通过约定好的采样频率、量化位数、声道数等信息,调用PyAudio函数库接口进行播放。回放功能是将图片流保存成avi视频格式,音频流保存成wav音频格式,然后在电脑文件系统中可以打开进行回放。通过多进程多线程将这三个功能配合起来,通过流水线操作的方式,让整个上位机程序执行更加的高效、稳定。上位机的程序流程图如下图所示。

220501-毕业设计-3.png

FPGA各模块功能实现

时钟模块实现

系统时钟输入和PLL配置

下图为FPGA开发板硬件原理图中的时钟部分,FPGA系统时钟信号由一颗50MHz有源晶振,从FPGA的T2引脚传入,由该时钟通过PLL锁相环和分频器,得到各模块所需的时钟,为整个系统提供准确的时钟信号。

220501-毕业设计-4.jpg

锁相环(Phase Locked Loop, PLL)是一种反馈型的控制电路,可以通过PLL对输入时钟进行系统级的时钟控制,可以配置管理时钟相位、偏移,具有倍频、分频和可编程占空比等功能。由于本设计使用到的模块较多,使用单一的时钟通过软件分频无法得到精准音频采样的频率,所以满足本设计要求。故使用PLL模块来满足该设计的不同时钟频率和不同时钟相位偏移的要求。通过Quartus提供的PLL IP核,对Altera FPGA片上的可编程PLL进行控制,使其输出各种时钟信号提供给各个模块使用。

下图为在Quartus 18.0中通过PLL IP核配置输入50Mhz时钟,输出2.205MHz时钟提供给麦克风模块的采样频率使用,配置软件会根据输入输出时钟频率自动计算出PLL的各个参数(时钟倍频参数、时钟分频参数),提供给麦克风模块的采样频率不需要相位偏移和特殊的占空比,故设置相位偏移为0,占空比为50%。

220501-毕业设计-5.jpg

软件分频器实现

由于片上PLL资源数量有限,部分对时钟精准度要求不高的模块也可以采用软件分频器实现。例如I²C模块中的时钟信号采用250KHz,通过对输入的50MHz系统时钟上升沿或者下降沿计数,计数器值累加到100时,让输出信号产生翻转,即可产生250KHz的I²C驱动信号。这就是软件分频器的实现原理,占用硬件资源少,像这种整数倍分频,也可以提供较好的精度。

数据交互模块实现

设计FPGA各模块交互时,需要将图像和音频的数据传送给网络模块封装发送,故使用FIFO来做数据的缓冲。而网络模块的时钟信号是125MHz,与图像传感器模块24MHz和麦克风模块44100Hz有较大的区别,所以不能直接将数据通过同读写同时钟的FIFO传送给网络模块发送。FIFO模块可以配置读写相同时钟和读写不同时钟,在实际测试中,由于读取时钟为125MHz高频率信号,读写不同时钟情况下,FIFO的读写会产生严重的数据错误。因此本设计采用读写同时钟,FIFO模块输入时钟为网络模块的时钟125MHz,再手动编写跨时钟数据交互的时序逻辑,实现不同时钟域数据通过FIFO的转换。

FIFO模块配置

FIFO(First In First Out)模块是对数据缓冲时用到存储器,使模块可以被突发性读写。通常也被用于高速信号跨时钟域的数据交互,它可以被顺序写入,然后可以被顺序读出,先进先出的特性是FIFO不同于其他存储器的地方。

在Quartus 18.0中也可以通过IP核的方式来配置FIFO模块,关系FIFO容量的两个参数是FIFO的宽度和深度,宽度是指同时多少位可以被读写,深度是指可以存储多少该宽度的数据。由于FPGA中以太网是以字(4Bytes)的宽度来发送数据,并且以太网一帧默认最长大小是1500个字节,其中有用的数据为1472个字节,因此这里选择宽度为32bits,深度为512words可以满足缓冲一阵的以太网数据包。这里配置时我们采用读写相同时钟,从而使FIFO高速稳定的运行。需要打开usedw[]的功能,让网络模块读取已经使用的FIFO数量,当已经使用的数量大于等于1472字节时,可以开始发送一帧以太网数据。在输出寄存器一选项中,选择要求时间同步,Yes(best speed),由于FIFO时钟是125MHz,不选速度优先的话也会导致FIFO读写混乱错误。同时关闭上下溢出检测以提升FIFO模块性能。下图为最后FIFO模块配置的部分参数,配置时一定要使性能最优。

220501-毕业设计-6.jpg

将数据写入FIFO模块实现

FIFO模块的时钟为125MHz,在图像FIFO存储模块中,图像传感器的时钟是24MHz,并且是8位宽度,需要手动编写跨时钟数据交互的时序逻辑,实现不同时钟域数据通过FIFO的转换。图像FIFO写模块输入有时钟、复位、图像数据、数据有效使能、垂直同步信号,输出有32位FIFO写入、FIFO写使能信号。捕获四个数据有效使能信号上升沿,将4个8位数据合并成1个32位数据,并向FIFO模块发送写使能,由于FIFO模块的时钟为125MHz,要写1个数据到FIFO模块,写使能信号高电平时间只能为8ns。为了使每幅图像不会等到下一张图像数据写入FIFO,直到FIFO满1472字节才发送,即使每幅图像都可以在该图像接收结束被发送完成,在垂直同步(一幅图像数据结束)信号产生后就向FIFO中追加写入1472个字节的0x00数据。

麦克风模块和网络模块也通过FIFO传递数据,麦克风模块的采样频率为44100Hz,也需要做个时钟转换和数据宽度转换,转换成FIFO模块的125MHz和32位宽才能写入FIFO,实现方法与图像数据写入FIFO模块类似。由于音频数据的采样量化位数是24位,转换成32位数据需要在首位补0x00,为了上位机方便保存成wav格式,采用小段存储方方式写入FIFO模块。

从FIFO模块读取数据实现

网络模块在判断FIFO存储数量大于等于1472字节时会开始发送数据,发送数据时会请求读取FIFO模块数据,以太网发送数据的时钟是125MHz的,读取FIFO数据后要及时锁存,在锁存进入稳态后才可以被网络模块读取发送,否则由于FIFO发送速度过快,数据在信号线上容易未进入稳态被读取,造成读取和发送的数据出现错误。因为音频传送的数据量小,且实时性要求高,所以音频FIFO模块读取的优先级要设置比图像FIFO模块读取的优先级高。

图像传感器模块实现

I²C驱动模块实现

使用OV2640传感器,需要用SCCB类I²C总线配置OV2640传感器的寄存器。I²C总线协议由飞利浦公司发明,由一根数据线和一根时钟线构成,属于半双工同步通信,较常用的时钟速率有低速模式100KHz,高速模式400KHz,超高速模式3.4MHz。I²C总线协议支持一主多从,由于I²C总线协议中设备标识符占7位空间,所以I²C总线理论可以挂载128个设备,但实际考虑I²C总线上设备的驱动能力,只可以挂载5个左右从设备。

I²C总线协议发送起始位后开始通信,起始位的标识是时钟线为高电平时,数据线从高电转变为低电平。通信时发送的第一个字节高7位内容为从设备的设备地址,末一位为读或写请求,低标识写标志位,反之则是读标志位。I²C通信协议中每发送一个字节后一位(第9位),对应的从机接收到就要发送应答响应,即第9位需要将数据线拉低响应,若主机读取到第9位仍为高电平,则标识无对应从机应答。发送设备地址后,若从机有应答,则主从双方继续通信,若主机写请求,就可以发送数据,向从机对应寄存器地址写入数据,从机收到数据后会返回应答信号,若主机读请求,则从机会向主机发送对应地址的数据内容,主机在接收后也需要拉低数据线响应。通信结束后主机发送停止位信号,停止位的标识是时钟线保持高电平,数据线从低电平转变为高电平的状态。

在FPGA中实现I²C驱动相对别的通信方式(USART、SPI等)难度较高,因为I²C驱动存在有多个工作状态,需要采用复杂的有限状态机来实现I²C驱动。下图是FPGA程序的有限状态机状态转移图,I²C驱动部分程序的代码见附录。

220501-毕业设计-7.jpg

图像传感器配置模块实现

OV2640图像传感器有较多的寄存器,参考数据手册配置,在图像传感器配置模块中总共配置了201个寄存器,通过case语句,用查表的形式读取相应寄存器地址和寄存器数据,发送给I²C驱动模块,配置OV2640图像传感器。

先对传感器软复位,后根据本设计的需要配置寄存器,例如:分辨率采用UXGA模式,对输入时钟不分频,倍频系数2,配置JPEG输出,设置图像窗口大小和图像尺寸大小等。配置的程序代码和各寄存器的配置的值见附录。在配置完成后,图像传感器配置模块会发送给图像数据读取模块OV2640初始化完成信号。

图像数据读取模块实现

图像传感器数据通讯接口采用的是DCMI接口,在OV2640中DCMI接口是8位数据并口,并且配有行同步垂直同步信号。行同步信号高电平时为图片中一行像素点有效数据,低电平为无效数据,所以每当出现行同步信号上升沿时就是该幅图像传输新的一行标识位。垂直同步信号是一张图片中有效数据的标识,当垂直同步高电平时为有效,每当出现垂直信号的下降沿时,表示该幅图像传输完成,当出现垂直信号的上升沿时,表示新的一幅图像传输开始。

图像数据读取模块收到配置模块发来的初始化完成信号后开始采集数据,在OV2640传感器实际应用中,图像传感器传来的前几幅图像会有显示问题,所以将开始传来的数据丢弃,根据垂直同步信号计数,第10幅图像开始开始采集图像数据发送给写FIFO模块。数据读取模块实现的代码见附录。

麦克风模块实现

I²S总线数据读取模块实现

使用的MEMS硅麦克风采用的是I²S通信协议,当INMP441的L/R引脚为低电平时,INMP441提供单个左通道音频数据,时序图如下图所示。和I²C比较而言,I²S多了个WS接口,WS接口是串行数据声道选择,为低时左声道麦克风模块在I²S总线上发送数据,右声道是高阻态。WS为高时右声道麦克风模块在I²S总线上发送数据,而左声道是高阻态。

220501-毕业设计-8.jpg

根据时序图可以编写FPGA的I²S驱动,可见经过WS的一周期会得到一组声音信号,WS在SCK下降沿时跳变,在WS跳变后的第二个SCK上升沿可以读取音频数据的最高位,依次24个SCK后得到完整的24位左声道音频数据。设计FPGA程序时,一周期WS会有50次SCK上升沿,因此SCK的频率会比WS高50倍,要保证采样率为44100Hz,就需要通过PLL模块给麦克风模块提供2.205MHz的SCK信号。具体的I²S驱动代码见附录。

千兆以太网模块实现

千兆以太网模块是本设计FPGA部分实现起来最复杂和困难的模块,由于千兆以太网PHY芯片采用125MHz高速时钟和8位并口数据传输,容易导致数据传输时未进入稳定态,出现时序混乱的现象。网络模块由三个部分组成,分别是网络发送模块、网络接收模块、CRC-32校验模块,三个模块之间的连接关系RTL视图如下图所示。

220501-毕业设计-9.jpg

CRC-32校验模块实现

以太网帧组成如下图所示,它是由前导、帧起始定界符、以太网帧头、以太网数据、帧校验序列组成,其中网络中帧校验序列使用最广泛的是采用4字节的循环冗余校验方式,即CRC-32校验。

220501-毕业设计-10.jpg

CRC-32计算方式可以采用串行或并行计算,为了发挥FPGA并行优势,本设计采用并行的方式计算CRC-32校验码,并行CRC-32计算的表可以由工具生成。在FPGA设计中,该模块输入的值有时钟、使能、一字节数据,以太网每发送一字节数据,就要将发送的数据值发送给CRC-32校验模块,会根据上次CRC-32的结果和本次传来的数据值,同步更新CRC-32的值,最后输出为CRC-32校验的结果加在以太网帧的帧尾发送出去,如果目的主机校验CRC-32结果失败,则会在数据链路层丢弃该帧。具体的CRC-32校验模块实现代码见附录。

千兆以太网数据发送模块实现

实现以太网数据发送模块,要解决两个问题,需要发送的数据包含哪些信息,需要如何控制千兆以太网PHY芯片。

在CRC-32校验模块实现一节中介绍了以太网帧的组成结构,在发送目的MAC地址时使用ff-ff-ff-ff-ff-ff广播地址。以太网帧中的以太网数据段格式如下图所示,由于网络摄像机传输要满足实时性,且网络传输在局域网环境下环境不复杂,所以采用UDP协议传输数据,即在IP首部选择协议的地址中写入17表示UDP,生存时间一般为64,标识部分需要每次发送后累加,首部校验和是对IP首部内容累加保留末四位进行校验,在本设计中将源IP地址(FPGA的IP地址)设置为192.168.0.2,目的IP地址(电脑的IP地址)设置为192.168.0.3。以太网数据一般不超过1500字节,IP首部占了24字节,所以IP数据部分最大可为1476字节,其中IP数据的首四字节为用户自定义的帧序列号,用于上位机校验是否有丢包现象产生,所以实际每帧的IP数据中传输有用信息的只有1472个字节。

220501-毕业设计-11.jpg

下图为物理层芯片RTL8211的硬件原理图,可见RTL8211与FPGA的连接主要包含8个发送引脚、8个接收引脚,还有发送和接收的时钟引脚,MDC和MDIO属于配置RTL8211芯片寄存器的接口,在本设计中采用默认寄存器值,所以未用到配置引脚。RTL8211发送和接收时钟均为125MHz,上升沿采样,发送和接收均为8位并口,可以满足1000Mbps全双工通信。

220501-毕业设计-12.jpg

设计千兆以太网数据发送模块也是用有限状态机实现,使用了idle, start, make, send55, sendmac, sendheader, sendpicdata, sendmicdata, sendcrc共9个状态。在idle状态的时候,程序主要是初始化数据,并且检测图像FIFO和音频FIFO是否有超过1472个字节,其中先检测音频FIFO的使用量,若有超过1472个字节跳转到下个状态start。在start状态中会对IP首部进行配置,如果是图像FIFO大于1472字节,会配置端口号为8080,如果是音频FIFO大于1472字节,会配置端口号为8081,配置完成后会跳到下一个状态。make状态中会对IP首部计算校验和,并写入IP首部配置变量ip_header中,跳转至send55状态。在send55状态下,FPGA会和RTL8211通信,发送7字节前导码0x55和1字节帧起始界定符0xD5,发送完成后跳转到下个状态sendmac。在sendmac状态中会发送以太网帧头,包含目的MAC地址,源MAC地址,长度类型这些信息。下个状态sendheader,即发送start状态下存入ip_header变量中的IP首部信息,接下来进入发送数据的状态。发送数据的状态分两种一种是发送图像的状态,另一种是发送音频数据的状态,两个实现的方法一致,只是FIFO读请求所对应的FIFO不同,在发送数据状态中要先发送帧序列号,图像和音频的帧序列号是分开的,发送完帧序列号后循环请求读FIFO368次,将1472字节数据发送给RTL8211芯片,RTL8211会将从FPGA接收到的数据通过网线中的两对差分信号线发送到电脑网口。发送数据状态结束后进入最后一个sendcrc状态,该状态下会读取最后CRC-32的校验值发送给以太网PHY芯片,该状态结束后,表示一帧以太网数据传输结束,状态又跳转至空闲状态检测两个FIFO使用量。具体实现详见附录代码。

上位机功能实现

图像功能实现

接收和解析图像数据包功能实现

接收UDP的数据包使用了Python中socket库,在程序开始引用该库,通过socket.socket(socket.AF_INET, socket.SOCK_DGRAM)构造函数构造一个套接字,配置使用UDP协议,并且通过server.bind(‘192.168.0.3’, 8080)绑定该套接字对应的IP地址和端口号。接收UDP数据包通过函数server.recvfrom(BUFSIZE),其中BUFSIZE为缓冲大小,由于一帧以太网数据1472字节,这里将BUFSIZE设置为1472*1000,以实现每次接收缓冲区满足处理的速度。在每次接收UDP数据包后,首先会校验帧序列号,即数据区前四字节,帧序列号是否为上次接收的累加1,若帧序列号不连续则表示存在丢包的现象,通过打印verify error告诉用户帧序列号校验失败。处理图像数据时,传来的图像数据时JPEG编码格式,由于JPEG编码格式帧起始标识符值为0xFFD8、帧结束标识符值为0xFFD9,提取数据包中的有用数据就是通过find(0xff, index)函数找到0xff功能标识符,然后再判断的下一字节是否为0xD8,若判断为真即表示找到JPEG图像数据头,再找尾标识符0xFFD9,通过同样的方法找到数据尾标识符后,将JPEG图像数据包头到包尾数据通过fp.write(data)函数写入jpg格式的文件,就完成了一张图像的保存。

图像流实时播放功能实现

要实现图像流实时播放功能,需按照拍摄图像的帧率,连续读取保存在磁盘中的图像,再在屏幕上不断刷新显示。本设计上位机程序采用OpenCV函数库实现图像的读取,解码和显示的功能。首先,在程序的开头通过import cv2引用OpenCV库。通过img = cv2.imread(img_root + str(i)+’.jpg’, cv2.IMREAD_COLOR)读取jpg格式的文件,其中img_root为图像所在文件夹的名字,imread的第一个参数就是图像路径,将读取到的数据保存在mat对象img中。调用cv2.namedWindow(“video”, cv2.WINDOW_AUTOSIZE)创建一个显示图像的窗口,窗口名为video。最后通过cv2.imshow(“video”, img)函数,该函数可以让名为video的显示窗口中显示img图像信息。cv2.waitKey(55)函数是使该窗口显示时间为55ms,这个延时时间由视频的帧数决定。

220501-毕业设计-13.jpg

图像流存储功能实现

OpenCV库中有支持导出avi视频的函数,fourcc = cv2.VideoWriter_fourcc(*’XVID’)函数,其中XVID参数是指将视频以MPEG-4编码类型保存,保存成avi格式的文件。videoWriter = cv2.VideoWriter(‘./avi/‘ + str(j) + ‘.avi’, fourcc, fps, size)函数配置了视频保存的路径,编码格式,帧率和每帧图像的分辨率,size = (1600, 1200)图像为1600 * 1200的分辨率。通过videoWriter.write(img)函数可以将img图像信息传入,让OpenCV处理生成视频。每隔150张图像发布一个10秒钟时长的avi视频,通过函数videoWriter.release()实现avi视频的发布。每次发布成功后程序会打印release告诉用户,可以在avi文件夹下查看保存的回放视频。

音频功能实现

接收和解析音频数据包功能实现

接收音频数据的方式与接收图像数据方式相同,通过Python的socket库。在接收音频数据后做帧序列号校验,并提取有效部分。接收到的音频数据是24位的麦克风adc原始值,由于自己电脑声卡输出只支持16位,播放24位音频会没有声音,故在上位机上做了个24位转换为16位的操作,即提取高16位数据,保存到pcm文件中。使用20ms的数据缓冲时间,分别将20ms的pcm数据保存到电脑中。

音频流实时播放功能实现

上位机使用pyaudio库播放PCM音频,在程序的开始通过import pyaudio引用该库,读取已经保存在硬盘中的音频数据,通过p = pyaudio.PyAudio()初始化音频播放器,由于PCM是音频的原始数据,不包含量化位数、通道数、采样率等参数,所以,通过stream = p.open(format=p.get_format_from_width(2), channels=1, rate=44563, output=True)配置要播放音频的参数,并赋值给stream对象。通过stream.write(data)向stream播放器对象中写入音频数据,就可以完成音频的实时播放。

音频流存储功能实现

使用wave库,将pcm音频保存成wav格式的音频文件写入磁盘,为了方便回放,每10s中保存一个wav音频。通过wavfile = wave.open(‘./wav/‘ + str(j) + ‘.wav’, ‘wb’)打开一个wav文件,通过设置函数给wavfile对象设置音频的通道数、量化位数和采样率。最后通过wavfile.writeframes(data)就可以往wav文件中追加音频内容,每10s保存成一个文件。

通过多进程实现各个功能

多进程并行工作实现

为了让程序高效运行,发挥CPU的多核优势,采用多进程并行的思想来实现上位机的设计。使用多进程可以实现流水线架构,例如,让解码显示图像的操作不阻塞接收UDP数据。在本上位机中共使用了四个进程,分别是图像接收数据和解析数据进程、音频接收数据和解析数据进程、图像实时显示和保存回放进程、音频实时播放和保存回放进程,四个进程互不影响,并行运行。在Python中使用multiprocessing库可实现多进程,例如创建接收和保存数据的进程p1 = multiprocessing.Process(target=receive_save_process, args=(pipe[0], )),第一个参数是传入进程所执行的函数,第二个参数是所要执行的函数对外的参数接口。通过p1.start()就可以使进程开始运行。

数据包进程间通信实现

在Python中进程间通信的常用方式有文件IO,共享内存,管道,消息队列等。通过文件的方式内存通信比较占用IO资源,为了使视频有更好的实时性,本设计采用管道和IO流的方式进程间通信。通过fp = io.BytesIO()创建IO流让fp指向该字节流,fp可以像文件一样通过write函数被写入,通过read函数读取IO流中的数据。多进程库提供了管道这种通信方式,通过multiprocessing.Pipe()创建管道。在多进程通信中,将数据写入IO流,并通过管道传输IO流的地址,使数据可以在内存中被交互,从而达到减小传输时延,使视频更具实时性。

系统性能测试与功能展示

系统性能测试

FIFO和以太网高带宽传输测试

FPGA编写测试模块,往FIFO模块中高速写入数据,再从FIFO中读取出进行对比,若对比不正确则亮红灯提示。测试的数据需要每次都不一致,竟可能有多个位产生变化,可以采用随机值。噪声的ADC值可以作为硬件真随机值,在本设计中通过麦克风传感器的低八位ADC值来作为测试数据,通过测试FIFO可以在125MHz频率下全速读写。

以太网测试也是采用比对发送和接收到的数据,由于发送和接收数据无法简单的通过其他的方式高速传输,所以要发送的数据选择有规律的数据。数据选择从0x00开始,下一字节都发送上一个数据加一后的补码,这样可以使每次发送的数据较上一次数据有更多的位会产生变化,通过此方法可以检测出更多的异常数据,若只通过单纯的数据累加测试,无法发现异常数据。经测试,以太网数据在200Mbps带宽下有较好的准确性,丢包现象不常发生,以太网在P2P模式下,全速传输千兆带宽数据时,丢包率为2%左右。

系统稳定性测试

对本设计经过长时间的稳定性测试,本设计不会产生图像卡顿,回放保存失败等现象,说明本设计具有良好的系统稳定性。

作品展示

下图为FPGA部分的图片,包含FPGA开发板、图像传感器和麦克风传感器。

220501-毕业设计-14.jpg

总结与不足

毕业设计是对学生四年所学知识的一次考察,也是对我们四年学习成绩的一次考验。本次毕业设计让我学习了FPGA设计和上位机开发,经过此次毕业设计,我懂得了如何将一整个设计划分成多个小功能逐个实现,在联调的过程中也学习了如何分析问题和解决问题。通过本次毕业设计,让我懂得了学习其实是长期积累的过程,越往深的学会发现自己有越多的不会,所以在今后工作是也要学会敬畏知识,学习是个长期积累的过程,就算毕业工作会我也会持续学习,努力提高自己的综合水平。

本次毕业设计还有几个可以改进的地方:第一,在传输UDP的时候为采用丢包重传,上位机具备发现丢包的能力,可以在丢包发生时将帧序列号发回FPGA,请求FPGA重发数据。第二,上位机软件没有图形化界面,可以使用QT对上位机的图形化界面做开发。第三,FPGA可以尝试设计个MCU,在MCU中跑操作系统和lwip协议栈,使用TCP协议传输语音数据。本设计还有更多功能待完善。

声明

此文章是搬运来的,原地址点击此处,写的真好,能看的非常懂,原作者有个地方可能笔误写错了,做了改正。

输入IO

这里所说的输入IO,指的是只作为输入,不具有输出功能。此时对于input引脚的要求就是高阻(高阻与三态是同一个概念)。基本输入电路的类型大致可以分为3类:基本输入IO电路、施密特触发输入电路以及弱上拉输入电路。

先从最基本的基本输入IO电路说起,其电路如图 1所示。

220426-GPIO输入输出各种模式-1.jpg

其中的缓冲器U1是具有控制输入端,且具有高阻抗特性的三态缓冲器。通俗地说就是这个缓冲器对外来说是高阻的,相当于在控制输入端不使能的情况下,物理引脚与内部总线之间是完全隔离的,完全不会影响内部电路。而控制输入端的作用就是可以发出读Pin状态的操作指令。其过程如图 2所示。

220426-GPIO输入输出各种模式-2.jpg

这种基本电路的一个缺点是在读取外部信号的跳变沿时会出现抖动,如下图所示。

220426-GPIO输入输出各种模式-3.jpg

于是施密特触发输入电路就是解决了上述这种抖动的问题,其经过施密特触发器后的信号如图 4所示。

220426-GPIO输入输出各种模式-4.jpg

对于输入电路还存在另外一个问题,就是当输入引脚悬空的时候,输入端检测到的电平是高还是低?当输入信号没有被驱动,即悬空(Floating)时,输入引脚上任何的噪声都会改变输入端检测到的电平,如图 5所示。

220426-GPIO输入输出各种模式-5.jpg

为了解决这个问题,可以在输入引脚处加一个弱上拉电阻,如图 6所示。

220426-GPIO输入输出各种模式-6.jpg

这样,当输入引脚悬空时,会被RP上拉到高电平,在内部总线上就有确定的状态了。

但是这种结构是有一定问题的。首先很明显的一点是,当输入引脚悬空时读到的是1,当输入引脚被高电平驱动时读到的也是1,只有当输入引脚被低电平驱动时读到的才是0。也就是对于读1采取的方式是”读取非零”的方式。

另一个问题是该电路对外呈现的不是高阻,某种意义上说也在向外输出,当外部驱动电路不同时可能出现错误的检测结果。例如外部驱动电路是如图 7所示的结构,该电路结构中通过K打到不同端可以输出高电平或者低电平。

220426-GPIO输入输出各种模式-7.jpg

如果将如图 7所示的电路输出低电平,连接到带有弱上拉电阻的输入引脚,其结构如下所示。

220426-GPIO输入输出各种模式-8.jpg

由欧姆定律知,测试点处的电平是4.545V,于是CPU测得的输入信号为高,而外部驱动电路希望输出的电平为低。这种错误的原因就在于这种结构的输入电路并不是真正的高阻,或者说这个输入IO其实也在输出,而且影响了外部输入电路。

这种情况的发生也说明了:信号前后两级传递,为什么需要输出阻抗小,输入阻抗大的原因。在这个例子中,外围驱动电路的输出阻抗很大,达到了100Kohm;而输入端的阻抗又不够大,只有10Kohm,于是就出现了问题。如果输入端的输入阻抗真正做到高阻(无穷大),如下所示,就不会出现问题。

220426-GPIO输入输出各种模式-9.jpg

上面提到的这个带弱上拉的输入电路,也就是在后续章节会提到的准双向端口的情况。

输出IO

IO输出电路最主要的两种模式分别是推挽输出(Push-Pull Output)和开漏输出(Open Drain Output)。

推挽输出(Push-Pull Output)

推挽输出的结构是由两个三极管或者MOS管受到互补信号的控制,两个管子始终保持一个处于截止,另一个处于导通的状态。如图 10所示。

220426-GPIO输入输出各种模式-10.jpg

推挽输出的最大特点是可以真正能真正的输出高电平和低电平,在两种电平下都具有驱动能力。

补充说明:所谓的驱动能力,就是指输出电流的能力。对于驱动大负载(即负载内阻越小,负载越大)时,例如IO输出为5V,驱动的负载内阻为10ohm,于是根据欧姆定律可以正常情况下负载上的电流为0.5A(推算出功率为2.5W)。显然一般的IO不可能有这么大的驱动能力,也就是没有办法输出这么大的电流。于是造成的结果就是输出电压会被拉下来,达不到标称的5V。

当然如果只是数字信号的传递,下一级的输入阻抗理论上最好是高阻,也就是只需要传电压,基本没有电流,也就没有功率,于是就不需要很大的驱动能力。

对于推挽输出,输出高、低电平时电流的流向如图 11所示。所以相比于后面介绍的开漏输出,输出高电平时的驱动能力强很多。

220426-GPIO输入输出各种模式-11.jpg

但推挽输出的一个缺点是,如果当两个推挽输出结构相连在一起,一个输出高电平,即上面的MOS导通,下面的MOS闭合时;同时另一个输出低电平,即上面的MOS闭合,下面的MOS导通时。电流会从第一个引脚的VCC通过上端MOS再经过第二个引脚的下端MOS直接流向GND。整个通路上电阻很小,会发生短路,进而可能造成端口的损害。这也是为什么推挽输出不能实现” 线与”的原因。

开漏输出(Open Drain Output)

常说的与推挽输出相对的就是开漏输出,对于开漏输出和推挽输出的区别最普遍的说法就是开漏输出无法真正输出高电平,即高电平时没有驱动能力,需要借助外部上拉电阻完成对外驱动。下面就从内部结构和原理上说明为什么开漏输出输出高电平时没有驱动能力,以及进一步比较与推挽输出的区别。

首先需要介绍一些开漏输出和开集输出。这两种输出的原理和特性基本是类似的,区别在于一个是使用MOS管,其中的”漏”指的就是MOS管的漏极;另一个使用三极管,其中的”集”指的就是MOS三极管的集电极。这两者其实都是和推挽输出相对应的输出模式,由于使用MOS管的情况较多,很多时候就用”开漏输出”这个词代替了开漏输出和开集输出。

介绍就先从开集输出开始,其原理电路结如图 12所示。

220426-GPIO输入输出各种模式-12.jpg

图 12左边的电路是开集(OC)输出最基本的电路,当输入为高电平时,NPN三极管导通,Output被拉到GND,输出为低电平;当输入为低电平时,NPN三极管闭合,Output相当于开路(输出高阻)。高电平时输出高阻(高阻、三态以及floating说的都是一个意思),此时对外没有任何的驱动能力。这就是开漏和开集输出最大的特点,如何利用该特点完成各种功能稍后介绍。这个电路虽然完成了开集输出的功能,但是会出现input为高,输出为低;input为低,输出为高的情况。

图 12右边的电路中多使用了一个三极管完成了”反相”。当输入为高电平时,第一个三极管导通,此时第二个三极管的输入端会被拉到GND,于是第二个三极管闭合,输出高阻;当输入为低电平时,第一个三极管闭合,此时第二个三极管的输入端会被上拉电阻拉到高电平,于是第二个三极管导通,输出被拉到GND。这样,这个电路的输入与输出是同相的了。

接下来介绍开漏输出的电路,如图 13所示。原理与开集输出基本相同,只是将三极管换成了MOS而已。

220426-GPIO输入输出各种模式-13.jpg

接着说说开漏、开集输出的特点以及应用,由于两者相似,后文中若无特殊说明,则用开漏表示开漏和开集输出电路。

  1. 开漏输出最主要的特性就是高电平没有驱动能力,需要借助外部上拉电阻才能真正输出高电平,其电路如图 14所示。

    220426-GPIO输入输出各种模式-14.jpg

当MOS管断开时,开漏输出电路输出高电平,且连接着负载时,电流流向是从外部电源,流经上来电阻RPU,流进负载,最后进入GND。

  1. 开漏输出的这一特性一个明显的优势就是可以很方便的调节输出的电平,因为输出电平完全由上拉电阻连接的电源电平决定。所以在需要进行电平转换的地方,非常适合使用开漏输出。
  2. 开漏输出的这一特性另一个好处在于可以实现”线与”功能,所谓的”线与”指的是多个信号线直接连接在一起,只有当所有信号全部为高电平时,合在一起的总线为高电平;只要有任意一个或者多个信号为低电平,则总线为低电平。而推挽输出就不行,如果高电平和低电平连在一起,会出现电流倒灌,损坏器件。

推挽与开漏输出的区别

220426-GPIO输入输出各种模式-15.jpg

双向IO

很多处理器的引脚可以设置为双向端口,双向端口的要求就是既可以输出信号,又可以读回外部信号输入。要同时做到这两点从原理上来说有点困难,首先从处理器的开漏输出IO口的内部结构说起,如图 16所示。

220426-GPIO输入输出各种模式-16.jpg

该结构是在图 13的基础上,在三极管之前加入了一个FF,目的是用于控制输出信号的时间。比较常见的一个应用场合是多个IO作为一个总线时,需要总线上的各个引脚同时将数据输出。

对于开漏输出结构,会将FF的输出Q端连接会输入驱动缓冲器,这样的话执行读操作是读的并不是外部引脚的状态,而是自己输出的状态。

双向开漏IO

但是对图 16的结构稍作修改,如图 17所示时,该结构称为双向开漏IO的结构。所做的改动是将输入驱动缓冲器连接到了PIN上。

220426-GPIO输入输出各种模式-17.jpg

该结构输出为”1”时,T1断开,此时pin对外呈现高阻,作为输入引脚没有任何问题。但是如果该结构输出”0”时,T1导通,此时pin对外短路到地,即无论外部输入什么信号,U2读回的全部是低。所以对于这样的结构,如果需要作为输入引脚使用时,必须给U1输出”1”后才能读取外部引脚数据。

准双向开漏IO

很多文献中还提到了准双向端口,其实准双向端口就是图 17的结构中加了一个上拉电阻,如图 18所示。

220426-GPIO输入输出各种模式-18.jpg

这个结构与图 17相比有以下相同与不同之处:

  1. 作为输入引脚使用时,也必须先向U1中写”1”,以达到断开T1的目的。所以是否需要提前写”1”并不是双向IO与准双向IO的区别。两者做输入端口时都需要提前写”1”。
  2. 双向端口作为输入时是真正的高阻态,而准双向IO作为输入端口时,输入阻抗不为高阻,于是有可能出现如本文图 8所示的问题。
  3. 准双向端口读取输入状态,默认为高。也就是判断外部输入信号的方法是”非低则为高”。即该结构只能准确的识别外部的低电平,无法区分悬空和真正的高。于是只要读到的不是0,都认为外部为1。

推挽输出作为双向IO

如果双向端口中的输出部分采用的是推挽输出结构,那么作为输入时必须将上下两个管子全部端口才能成为高阻,作为输入。

51单片机的P0端口

在双向端口的讨论中,比较复杂的就是51单片机的P0端口了。这里就详细讨论一下51单片机的P0端口结构和工作原理。

P0端口的内部结构如图 19所示。

220426-GPIO输入输出各种模式-19.jpg

内部结构比较复杂,包括以下这些器件:

  1. U1:与门。一个输入连着控制线,另一个输入连接这地址/数据信号。由于与门的特性,当控制线为1时,与门输出与地址/数据信号的电平保持一致;如果控制线为0,则输出恒为。于是控制信号线相当于与门的使能信号。
  2. U2:反相器,输出信号为地址/数据信号的反相信号。
  3. U3和U6都是具有控制输入端且具有高阻抗特性的三态缓冲器,作用是对于外部呈现高阻态。当控制端使能时可以将外部信号的电平读进数据总线。
  4. U4:为锁存器,目的就是控制引脚输出信号的时间。
  5. U5:模拟开关,可以控制V2的输入信号是来自锁存器U4的Q非输出还是来自于反相器U2的输出。
  6. V1和V2分别是两个MOS管。

了解了各个独立器件之后就开始介绍工作在各个模式下的工作原理:

P0用于地址/数据线时:

在P0作为地址/数据线时,是地址、数据复用总线,P0需要输出地址,同时需要读回数据信号。

当P0需要输出地址信息时,U1的控制信号为0,模拟开关U5接到U2反相器的输出。于是当地址信号线传来的信号为1,与控制线”1”相与之后输出到V1的输入信号为”0”,V1截止。地址信号”1”经反相之后,通过模拟开关输出到V2的输入端为”1”,V2导通,于是情况如图 20所示,pin输出”0”。

220426-GPIO输入输出各种模式-20.jpg

当地址信号线传来的信号为1,与控制线”1”相与之后输出到V1的输入信号为”1”,V1导通。地址信号”0”经反相之后,通过模拟开关输出到V2的输入端为”0”,V2截止,于是情况如图 21所示,pin输出”1”。

220426-GPIO输入输出各种模式-21.jpg

于是在作为地址线输出时,V1、V2两个MOS管均使用了,是推挽输出。

当P0在输出低8位地址信息后,将变为数据总线,此时CPU的操作是控制端输出0,模拟开关打到锁存器的Q非端,且向锁存器中打入”1”。于是Q非输出为0,V2截止。同时控制线为0使得与门输出为0,V1截止。由于V1和V2都截止,所以此时pin对外完全呈现高阻,作为输入端口,外部数据通过U6进入内部总线,情况如图 22所示。(相当于将推挽输出的两个MOS管全部断开了)此时由于对外呈现高阻,所以是真正的输入引脚。这就解释了为什么说P0是真正的双线端口。

220426-GPIO输入输出各种模式-22.jpg

P0用于普通IO时:

在P0作为普通IO并作为输出时,控制信号为0,使V1始终处于截止状态。模拟开关连接到Q非输出,当作为输出时,锁存器的输入端直接输入0或者1,Q非将反相信号输入到V2的输入端。即当输出”0”时,V2输入端为”1”,V2导通,pin输出”0”;当输出”1”时,V2输入端为”0”,V2截止,pin输出高阻的0。即当P0工作在普通IO模式下,输出为开漏输出,且内部没有上拉电阻。

在P0作为普通IO并作为输入时,控制信号为0,使V1始终处于截止状态。模拟开关连接到Q非输出,且CPU自动向锁存器输入端写1,则V2输入端为0,V2截止。与之前在作为地址/数据线,作为输入时一样,也是两个MOS管全部断开,pin直接连接到U6,对外呈现高阻。于是也是真正的输入引脚。

综上P0无论工作在哪种模式下都是真正的双端口IO。

51单片机的P1~P3端口

51单片机的其他三个端口的内部结构如图 23所示,与P0相比简单了很多,没有了顶部的MOS管,也没有了地址/数据信号的选项。作为输出时是带有上拉电阻的的开漏输出,作为输入时是有上拉电阻存在的,于是输入端口对外不是高阻。这就解释了为什么P1~P3只能是准双向端口。

220426-GPIO输入输出各种模式-23.jpg

数组指针&指针数组

参考:https://www.cnblogs.com/mq0036/p/3382732.html,两者内存分布写的很详细。

数组指针

定义int (*p)[n];

()优先级比[]高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。

如要将二维数组赋给一指针,应这样赋值:

1
2
3
4
int a[3][4];
int (*p)[4]; //该语句是定义一个数组指针,指向含4个元素的一维数组。
p = a; //将该二维数组的首地址赋给p,也就是a[0]或&a[0][0]
p++; //该语句执行过后,也就是p=p+1;p跨过行a[0][]指向了行a[1][]

所以数组指针也称指向一维数组的指针,亦称行指针。

指针数组

定义int *p[n];

[]优先级比*高,先与p结合成为一个数组,再由int*说明这是一个整型指针数组,它有n个指针类型的数组元素。这里执行p+1时,则p指向下一个数组元素,这样赋值是错误的:p=a;因为p是个不可知的表示,只存在p[0]、p[1]、p[2]…p[n-1],而且它们分别是指针变量可以用来存放变量地址。但可以这样 p=a; 这里p表示指针数组第一个元素的值,a的首地址的值。
如要将二维数组赋给一指针数组:

1
2
3
4
5
int *p[3];
int a[3][4];
p++; //该语句表示p数组指向下一个数组元素。注:此数组每一个元素都是一个指针
for(i=0;i<3;i++)
p[i] = a[i];

这里int *p[3] 表示一个一维数组内存放着三个指针变量,分别是p[0]、p[1]、p[2],所以要分别赋值。

常量指针&指针常量

指针常量

定义int * const p = &a;

p是个常量,p的类型是int*,所以p的值不能改变,但是*p的值可以改变。

常量指针

定义const int * p = NULL;

*p是个常量,p的类型是int*,所以*p的值不能改变,但是p的值可以改变。利用这个特性,在函数传参时使用常量指针可以防止函数对变量值的误修改。

常量指针常量

定义const int * const p = &a;

p是个常量,*p也是个常量,p的类型是int*,所以*p的值不能改变,p的值也不能改变。

引用&常量引用

引用

定义int& r = a;

C++ 引用,引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字。

函数可以通过引用传参,void foo(int &a){a = 1;}修改引用形参可以改变实参。

常量引用

定义const int& r = a;

函数可以通过常量引用传参,void foo(const int &a) {;}只能读取a,不能修改a。利用这个特性,在函数传参时使用常量指针可以防止函数对变量值的误修改。

函数指针&指针函数

函数指针

定义函数指针void (*pFun)(int a, int b);

定义函数void Fun(int a, int b) {;}

将Fun函数的首地址赋给指针变量pFunpFun = Fun;也可以是pFun = &Fun;

通过指针函数调用pFun(1, 2);也可以是(*pFun)(1, 2);

函数指针定义时返回值,参数类型,个数都必须与要指向的函数相同。

指针函数

指针函数是返回类型为指针的函数。

C结构体内存对齐

课程链接

课程笔记

L1~L3

  1. 课程目标:进入操作系统,学习操作系统的运作。

  2. 计算机工作方式:取址执行,取CS和PC指针指向内存中的指令,在CPU执行

  3. 操作系统引导:启动引导时内核在内存中的位置和移动后的位置情况图220326-操作系统-1.jpg

    开机时,CS=0xffff,IP=0x0000,CPU处于实模式。实模式寻址CS<<4+IP,CS存放段地址,IP存放4位段偏移量,共20位。0xffff0地址属于BIOS映射区,在BIOS里检查硬件(RAM,键盘,…);

    将磁盘0磁道0扇区(引导扇区)读到内存0x7c00,引导扇区代码存放在bootsect.s;

    将引导扇区代码从内存0x7c00处移动到0x90000处;

    jmpi go, INITSEG,INITSEG=0x9000,跳至bios的go标号处;

    jmpi 0, SETUPSEG,SETUPSEG=0x9020,跳至setup.s;

    在setup.s start:中读取扩展内存大小,读取显卡参数,…;

    do_move:中将system模块移动到0地址,jmpi 0, 8,跳转到system模块,system模块开始是head.s;

    通过设置cr0最后一位,PE=1由16位实模式转到32位保护模式,在保护模式下,CS表示选择子,查GDT表得到;

    在head.s中初始化GDT表和IDT表,setup_paging执行ret后,会执行函数main(),进入main()后的栈为0,0,0,L6,三个0表示main函数的参数,L6表示main函数返回时进入死循环;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    after_page_tables:
    pushl $0 # These are the parameters to main :-)
    pushl $0 # 这些是调用 main 程序的参数(指 init/main.c)。
    pushl $0 # 其中的'$'符号表示这是一个立即操作数。
    pushl $L6 # return address for main, if it decides to.
    pushl $_main # '_main'是编译程序对 main 的内部表示方法。
    jmp setup_paging # 跳转至第 198 行。
    L6:
    jmp L6 # main should never return here, but
    # just in case, we know what happens.
    # main 程序绝对不应该返回到这里。不过为了以防万一,
    # 所以添加了该语句。这样我们就知道发生什么问题了。
    setup_paging:
    设置页表...
    ret

L4~L7

  1. 操作系统接口:接口表现为函数调用,又由系统提供,所以称为系统调用,可移植操作系统接口(Portable Operating System Interface of uniX, POSIX)
  2. 系统调用的实现:应用层要通过接口访问内核,不能直接读内核内存,内核是受保护的。中断是进入内核的唯一方法;220326-操作系统-2.jpg220326-操作系统-3.jpg
  3. 课程任务:操作系统要学习CPU管理、内存管理、文件管理;

L8~L19

  1. CPU管理:多道程序,交替执行(并发)。运行的程序是进程,多进程切换时要保存现场。通过进程控制块(Process Control Block, PCB)来记录进程信息。操作系统要把这些进程记录好,要按照合理的次序分配资源、调度;

  2. 进程状态图:220326-操作系统-4.jpg

  3. 通过进程内存映射表来实现内存分离,切换进程时要切换内存映射表;通过上锁,来保证共享内存的正确被读写;

  4. 用户级线程和核心级线程区别:用户级线程创建、切换无需进内核,一个线程阻塞进程会被切换;

  5. 线程的优点:线程切换无需切换内存映射表。220326-操作系统-5.jpg

  6. 用户级线程切换通过Yield(),要先切换栈,栈会存在TCB(线程控制块)中,每个线程对应一个TCB;

  7. 核心级线程切换时除了用户栈还有内核栈也要一起切换。在内核阻塞时要进行TCB切换,使用switch_to(cur, next);TCB切换完后根据TCB完成内核栈切换,最后IRET返回用户态,切换用户栈;

  8. 前台任务需要响应时间小->导致切换次数多->导致系统内耗大->导致吞吐量(完成任务的量)小。后台任务更加关注周转时间。任务可分成IO约束型任务和CPU约束型任务;

  9. CPU调度策略:FIFO、Priority;CPU调度算法:先来先服务(First come, first served),短作业优先(SJF)这个算法周转时间最小,按时间片来轮转调度(RR),优先级调度(前台>后台);

  10. linux0.11中的调度函数既考虑了优先级又考虑了时间片:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    void Schedule(void) // 在 kernel/sched.c 中
    { while(1){ c=-1; next=0; i=NR_TASKS; p=&task[NR_TASKS];
    while(--i){ if((*p->state == TASK_RUNNING&&(*p)->counter>c)
    c=(*p)->counter, next=i; }
    if(c) break; // 找到了最大的counter,跳出,执行switch_to()
    for(p=&LAST_TASK;p>&FIRST_TASK;--p)
    (*p)->counter=((*p)->counter>>1)+(*p)->priority;
    }
    switch_to(next);}
  11. 进程间同步看信号量(Semaphore),可使多个进程合理有序运行。读写信号量的代码一定在临界区,或者原子操作。临界区:一次只允许一个进程进入该进程的那段代码。原子操作:不会被线程调度机制打断的操作;

  12. 保护临界区的方法:关闭中断cli();,临界区,开中断sti();,剩余区。这种方法只适用于小系统,不适用于多核CPU。还可以采取硬件原子指令法,硬件一条指令修改mutex变量;

  13. 信号量未互斥使用会造成死锁。死锁处理:死锁预防,死锁避免,死锁检测+恢复,死锁忽略;

L20~L25

  1. 内存使用:将程序放到内存中,PC指向开始的地址。将程序从硬盘载入内存需要重定位,物理地址=基址+逻辑地址。重定位可以在编译时(静态系统)、载入时执行、运行时执行。编译时重定位的程序只能放在内存固定位,载入时重定位的程序一旦载入内存就不能动了;220326-操作系统-6.jpg

  2. 程序载入后还需要移动,引入交换(swap)概念,可以把暂时不用的内存搬到交换分区,充分利用内存。

  3. 引入分段,程序由若干部分(段)组成,每个段有各自的特点、用途:主程序(只读)、变量集(可写、不会动态增长)、函数集、动态数组(会动态增长)、栈。使用分段思想可以让用户分治每个段,可以让内存更高效使用。定位具体指令mov [es:bx], ax。地址组成:<段号,段内偏移>。进程段表存放在LDT表,系统段表存放在GDT表,LDT存放在PCB中;220326-操作系统-7.jpg

  4. 内存分区可分为固定分区和可变分区。可变分区的管理:空闲分区表、已分配分区表。可变分区分配内存算法:首先适配(最快)、最佳适配和申请空间长度最接近(会导致内存碎片)、最差适配;

  5. 实际物理内存采用分页来管理,分区是对虚拟内存(交换分区)的管理方法。分区会造成内存碎片,分页会将物理内存分割(比如每4k分割),程序也会被分页,每段都会被分页,分页会使内存存储离散化。地址翻译有专门的硬件内存管理单元(MMU)执行;220326-操作系统-8.jpg

  6. 为了提高内存空间利用率,页应该小,但是页小了页表就大了。如果页表只存放用到的页,则需要顺序查找页表,速度慢。采用多级页表可兼顾速度和空间,即页目录表+页表,地址组成<10bits页目录号,10bits页号,12bits偏移>,12bits偏移刚好是4K。

  7. 多级页表增加了访存的次数,尤其是64位系统。硬件上引入转译后备缓冲区(Translation Look-aside Buffer,TLB,快表),快表能存放最近查过多级页表的逻辑页和物理页,可以根据页号直接查物理页号;220326-操作系统-9.jpg

  8. 段页结合:段面向用户,页面向硬件。实际的段页内存管理,程序如何载入内存,fork后内存做了什么可以看L23 段页结合的实际内存管理220326-操作系统-10.jpg

  9. 用内存换入换出实现“大内存”,虚拟内存4G,物理内存可以只有1G。当程序运行缺页时, page fault中断请求do_no_page调页(读磁盘),这就是换入;

  10. 内存换出:将内存中不用的页换出到磁盘。如何找不用的页?换出算法:FIFO(先来的先被换出,不太行)、MIN(将未来最远要使用的页淘汰,是最优方案,但无法预测未来)、LRU(Least Recently Used,最近最少使用);

  11. LUR的准确实现:可以用时间戳,每次地址访问都需要修改时间戳,需维护一个全局时钟,实现代价较较大,可以用页码栈,选择栈底淘汰,代价也太大。在实际中内存换出算法采用LUR的近似实现:时钟算法(环形队列)220326-操作系统-11.jpg220326-操作系统-12.jpg

  12. 给进程分配多少页框?分配多了,请求调页导致的内存高效利用就没有了。分配少了,会频繁请求调页导致系统低效。系统低效解释:系统内进程增多多,每个进程的缺页率增大大,缺页率增大到一定程度,进程总等待调页完成,CPU利用率降低低,进程进一步增多,缺页率更大,称这一现象为颠簸(thrashing);

L26~L32

  1. IO设备、显示器、键盘,让这些外设工作的基本思想就是往外设硬件寄存器写值,外设处理完再产生中断,CPU处理。在Linux中无论什么设备都用文件接口open、read、write、close,例如int fd = open("/dev/xxx");到最后是通过out写对应寄存器;

  2. 生磁盘的使用:磁盘驱动负责从盘块号(block)计算出柱面(cgl)、磁头(head)、扇区号(sec)。从CHS到扇区号,从扇区到盘块(第一层抽象):扇区号 = C×H×S+H×S+S220326-操作系统-13.jpg

  3. 多个进程通过请求队列使用磁盘(第二层抽象);220326-操作系统-14.jpg

  4. 访问磁盘时间 = 写入控制器时间 + 寻道时间(8~12ms)+ 旋转时间(7200rmp,半周4ms)+ 传输时间(50MB/s,约0.3ms)。可见访问磁盘主要时间花在寻道,block相邻的盘块可以快速读出,因此需要考虑寻道算法。寻道调度算法有FCFS(先来先服务)、SSTF(短寻道优先算法,可能有磁道长时间访问不到)、SCAN(SSTF+中途不折回)、C-SCAN(SCAN+直接移动到另一端,电梯算法);

  5. 引入文件(第三层抽象),文件是建立字符流在磁盘块集合的映射关系,每个文件都对应一个文件控制块(FCB),连续结构存储FCB包含文件名、起始块、块数的信息,链式结构存储FCB包含文件名、起始块的信息,链式结构存储顺序访问慢、可靠性差,索引结构存储文件是连续和链式的有效折中,FCB包含文件名、索引块的信息。在实际系统中采用的是多级索引结构,根据不同大小文件分成不同级索引通过inode一级级查询访问,因此可以标识很大的文件,很小的文件可以高效访问,中等大小的文件访问速度也不慢;

  6. 引入文件系统(第四层抽象),文件系统引入目录树,文件目录也是个文件,存放目录下其他文件名和对应文件的FCB指针。inode位图:哪些inode空闲,哪些被占用。盘块位图:哪些盘块是空闲的,硬盘大小不同这个位图的大小也不同。超级块:存放i节点位图和盘块位图的大小,因此可以计算出i节点起始地址;220326-操作系统-15.jpg220326-操作系统-16.jpg220326-操作系统-17.jpg

课程总结

通过本次操作系统的学习,了解了操作系统全图(CPU、内存、文件设备),学习了多进程视图、文件视图。哈工大的几个实验非常的难,Study OS by coding !!!

GDB介绍

GDB是什么

GDB: The GNU Project Debugger,GDB是GNU开源组织发布的一个强大的Linux下的程序调试工具,GDB主要帮助你完成下面四个方面的功能:

  • 启动你的程序,可以按照你的自定义的要求随心所欲的运行程序。
  • 可让被调试的程序在你所指定的调置的断点处停住。(断点可以是条件表达式)
  • 当程序被停住时,可以检查此时你的程序中所发生的事。
  • 你可以改变你的程序,将一个BUG产生的影响修正从而测试其他BUG。

GDB支持哪些语言

GDB主要来调试C/C++语言写的程序,当然也就可以调试其他语言程序,另外的语言没了解过。

GDB使用

在Ubuntu中安装GDB

1
2
sudo apt-get update
sudo apt install gdb

补一下GCC的知识

GCC参数详解,gcc 与 g++ 分别是 gnu 的 c & c++ 编译器 gcc/g++ 在执行编译工作的时候,总共需要4步:

  • 预处理,生成 .i 的文件[预处理器cpp],指令:-E

  • 将预处理后的文件转换成汇编语言, 生成文件 .s [编译器egcs],指令:-S

  • 由汇编变为目标代码(机器代码)生成 .o 的文件[汇编器as],指令:-c

  • 连接目标代码, 生成可执行程序 [链接器ld],指令:-o FILE

在配合GDB时,gcc的指令选项有:

  • -g(生成调试信息,GNU 调试器可利用该信息)
  • -ggdb(此选项将尽可能的生成 gdb 的可以使用的调试信息)

GDB中的基本调试命令

命令 命令缩写 命令说明
set args 设置主程序的参数。例如:./test 1 2设置参数的方法是:gdb test(gdb) set args 1 2
break b 设置断点,b 20 表示在第20行设置断点,可以设置多个断点。
run r 开始运行程序, 程序运行到断点的位置会停下来,如果没有遇到断点,程序一直运行下去。
next n 执行当前行语句,如果该语句为函数调用,不会进入函数内部执行。
step s 执行当前行语句,如果该语句为函数调用,则进入函数执行其中的第一条语句。注意了,如果函数是库函数或第三方提供的函数,用s也是进不去的,因为没有源代码,如果是您自定义的函数,只要有源码就可以进去。
print p 显示变量值,例如:p name表示显示变量name的值,print可以做运算。
continue c 继续程序的运行,直到遇到下一个断点。
set var name=value 设置变量的值,假设程序有两个变量:int ii; char name[21];set var ii=10 把ii的值设置为10;set var name=”abc” 把name的值设置为”abc”,注意,不是strcpy。
quit q 退出gdb环境。

进入GDB调试

1
2
3
4
gcc test.c -g -o test
gdb test # 进入gdb,指定test为可执行文件
# 也可以只执行gdb,在gdb中用file命令指定可执行文件
# 接下来就可以使用GDB的基本调试命令调试程序了

调试正在运行的程序

1
2
3
4
5
6
# 通过ps获取程序进程号
gdb test -p [进程号]
# 进入gdb后,程序会被暂停
(gdb)bt # 使用bt查看程序的调用栈
# Print backtrace of all stack frames, or innermost COUNT frames.
# 退出后,程序继续执行

调试多进程程序

1
2
3
4
5
6
(gdb)set follow-fork-mode [parent|child]
# (缺省是parent)调试父|子进程,父|子进程不受影响
(gdb)set datach-on-fork [on|off]
# (缺省是on)on表示调试当前进程时,其他进程继续运行,off表示调试当前进程时,其他进程被GDB挂起
(gdb)info inferiors # 查看调试的进程
(gdb)inferior # 切换当前调试的进程,进程id是查看调试的进程返回的Num值

调试多线程程序

1
2
3
4
5
6
7
8
ps aux|grep test # 查看当前运行的进程,查找符合"test"的字符串
ps -aL|grep test # 查看当前运行的轻量级进程
# 轻量级进程(LWP)是建立在内核之上并由内核支持的用户线程,它是内核线程的高度抽象,每一个轻量级进程都与一个特定的内核线程关联。
pstree -p [主线程ID] # 查看主线程和新线程的关系
(gdb)info threads # 查看线程
(gdb)thread [线程ID] # 切换线程,线程ID是查看线程返回的Num值
(gdb)set scheduler-locking [on|off] # on:只运行当前线程,off:运行全部的线程
(gdb)thread apply [线程ID|all] [cmd] # 让某个|全部线程执行某GDB命令

服务程序运行日志

设置断点或单步跟踪可能会严重干扰多进程、多线程之间的竞争状态,导致我们看到一个假象。一旦我们在某个线程设置了断点,该线程在断点处挺住了,只剩下另一个线程在跑。这时候,并发的场景已经完全被破坏了,通过调试器看到的只是一个和谐的场景(理想状态)。输出Log日志可以避免断点和单步跟踪所导致的副作用。

今晨的日出

220225-asmInC-1.jpg

早晨学习操作系统,看到系统调用实现时,遇到了C内嵌汇编,所以补了下相关知识。

__asm__

补充资料:https://dirtysalt.github.io/html/gcc-asm.html

在内嵌汇编中,可以将C语言表达式指定为汇编指令的操作数,而且不用去管如何将C语言表达式的值读入哪个寄存器,以及如何将计算结果写回C 变量,你只要告诉程序中C语言表达式与汇编指令操作数之间的对应关系即可,GCC会自动插入代码完成必要的操作。

完整的内嵌汇编格式:__asm__ __volatile__("Instruction List" : Output : Input : Clobber/Modify);

__asm__ 是GCC关键字asm的宏定义,asm用于声明这行代码是一个内嵌汇编表达式,任何内嵌的汇编表达式都以此关键字作为开头。

一个最简单的内嵌汇编:__asm__("nop"); 括号里的是汇编指令,表示运行空指令,一个机器周期。Instruction List 是汇编指令序列,它可以是空的。可以有多条汇编指令,需要将所有指令放在多对引号中,两条指令必须用换行或分号分开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int printk(const char *fmt, ...)
{
// ……
__asm__("push %%fs\n\t"
"push %%ds\n\t"
"pop %%fs\n\t"
"pushl %0\n\t"
"pushl $buf\n\t"
"pushl $0\n\t"
"call tty_write\n\t"
"addl $8,%%esp\n\t"
"popl %0\n\t"
"pop %%fs"
::"r" (i):"ax","cx","dx");
// ……
}

可以看上面printk()的例子,首先push %fs保存这个指向用户段的寄存器,在最后pop %fs将其恢复,printk()的核心是调用tty_write()

Output Input 用来指定当前内联汇编语句的输出与输入,格式为形如“constraint”(variable)的列表(逗号分隔),constraint是限制字符,下面表中列出几个常用限制字符作用,更多用法见:GCC内嵌汇编一些限制字符串

限制字符 作用
a/b/c/d/s/d 将输入变量放入eax/ebx/ecx/edx/esi/edi
q 将输入变量放入eax,ebx,ecx,edx中的一个
r 输入变量放入通用寄存器,也就是eax,ebx,ecx,edx,esi,edi中的一个
=/+ 操作数在指令中是只写/读写类型的(输出操作数)

__volatile__

__volatile__ 是GCC关键字volatile的宏定义,在内联汇编中,它是可选的。使用它会向GCC声明不允许对该内联汇编优化,否则当使用了优化选项(-O)进行编译时,GCC将会根据自己的判断决定是否将这个内联汇编表达式中的指令优化掉。

编译器优化常用的方法有:将内存变量缓存到寄存器;调整指令顺序充分利用CPU指令流水线,常见的是重新排序读写指令。对常规内存进行优化的时候,这些优化是透明的,而且效率很好。由编译器优化或者硬件重新排序引起的问题的解决办法是在从硬件(或者其他处理器)的角度看必须以特定顺序执行的操作之间设置内存屏障(memory barrier),linux 提供了一个宏,barrier(),解决编译器的执行顺序问题,但对硬件无效。

有一种硬件级别的优化:内存访问速度远不及CPU处理速度,为提高机器整体性能,在硬件上引入硬件高速缓存Cache,加速对内存的访问。在现代CPU中指令的执行并不一定严格按照顺序执行,没有相关性的指令可以乱序执行,以充分利用CPU的指令流水线,提高执行速度。

Clobber/Modify

有时候,你想通知GCC当前内联汇编语句可能会对某些寄存器或内存进行修改,希望GCC在编译时能够将这一点考虑进去。那么你就可以在Clobber/Modify域声明这些寄存器或内存。这种情况一般发生在一个寄存器出现在”Instruction List”,但却不是由Input/Output操作表达式所指定的,也不是在一些Input/Output操作表达式使用”r”约束时由GCC 为其选择的,同时此寄存器被”Instruction List”中的指令修改,而这个寄存器只是供当前内联汇编临时使用的情况。

例如:__asm__ ("mov R0, #0x34" : : : "R0");寄存器R0出现在”Instruction List中”,并且被mov指令修改,但却未被任何Input/Output操作表达式指定,所以你需要在Clobber/Modify域指定”R0”,以让GCC知道这一点。

因为你在Input/Output操作表达式所指定的寄存器,或当你为一些Input/Output操作表达式使用”r”约束,让GCC为你选择一个寄存器时,GCC对这些寄存器是非常清楚的——它知道这些寄存器是被修改的,你根本不需要在Clobber/Modify域再声明它们。但除此之外, GCC对剩下的寄存器中哪些会被当前的内联汇编修改一无所知。所以如果你真的在当前内联汇编指令中修改了它们,那么就最好在Clobber/Modify 中声明它们,让GCC针对这些寄存器做相应的处理。否则有可能会造成寄存器的不一致,从而造成程序执行错误。

如果一个内联汇编语句的Clobber/Modify域存在”memory”,那么GCC会保证在此内联汇编之前,如果某个内存的内容被装入了寄存器,那么在这个内联汇编之后,如果需要使用这个内存处的内容,就会直接到这个内存处重新读取,而不是使用被存放在寄存器中的拷贝。因为这个 时候寄存器中的拷贝已经很可能和内存处的内容不一致了。

这只是使用”memory”时,GCC会保证做到的一点,但这并不是全部。因为使用”memory”是向GCC声明内存发生了变化,而内存发生变化带来的影响并不止这一点。

1
2
3
4
5
6
7
8
9
int main(int __argc, char* __argv[]) 
{
int* __p = (int*)__argc;
(*__p) = 9999;
__asm__("":::"memory");
if((*__p) == 9999)
return 5;
return (*__p);
}

上面例子中,如果没有那条内联汇编语句,那个if语句的判断条件就完全是一句废话。GCC在优化时会意识到这一点,而直接只生成return 5的汇编代码,而不会再生成if语句的相关代码,而不会生成return (*__p)的相关代码。但你加上了这条内联汇编语句,它除了声明内存变化之外,什么都没有做。但GCC此时就不能简单的认为它不需要判断都知道 (*__p)一定与9999相等,它只有老老实实生成这条if语句的汇编代码,一起相关的两个return语句相关代码。

linux内核中内存屏障也是基于它实现,#define barrier() _asm__volatile_("" : : : "memory"),主要是保证程序的执行遵循顺序一致性。

Linux screen工具

功能介绍

Linux screen命令用于多重视窗管理程序。此处所谓的视窗,是指一个全屏幕的文字模式画面。通常只有在使用telnet登入主机或是使用老式的终端机时,才有可能用到screen程序。

语法

1
screen [-AmRvx -ls -wipe][-d <作业名称>][-h <行数>][-r <作业名称>][-s <shell>][-S <作业名称>]

参数说明

  • -A  将所有的视窗都调整为目前终端机的大小。
  • -d<作业名称>  将指定的screen作业离线。
  • -h<行数>  指定视窗的缓冲区行数。
  • -m  即使目前已在作业中的screen作业,仍强制建立新的screen作业。
  • -r<作业名称>  恢复离线的screen作业。
  • -R  先试图恢复离线的作业。若找不到离线的作业,即建立新的screen作业。
  • -s  指定建立新视窗时,所要执行的shell。
  • -S<作业名称>  指定screen作业的名称。
  • -v  显示版本信息。
  • -x  恢复之前离线的screen作业。
  • -ls或–list  显示目前所有的screen作业。
  • -wipe  检查目前所有的screen作业,并删除已经无法使用的screen作业。

常用命令

1
2
3
4
5
screen -ls  # 显示已创建的screen终端
screen -S <screen name> # 创建新的screen终端
screen -r <screen name> # 重连screen终端
screen -d <screen name> # 断开screen终端
screen -S <screen name> -X quit # 强制停止命令

screen快捷键

  • Ctrl -a c 创建新的视窗
  • Ctrl -a d 断开screen终端,任务还在运行
  • Ctrl -a k 删除当前视窗
  • Ctrl -a 空格 视窗切换
  • Ctrl -a ? 快捷键帮助
  • Ctrl -a : 命令模式,类似vi
  • Ctrl -a [ 复制模式,可以移光标上去看上面的打印

screen配置

1
wget https://github.com/hao0527/hao0527.github.io/blob/main/others/screencfg -O ~/.screenrc  # 一个好用的screen配置文件

上面这个配置指令有些问题,wget下载下来的是html,所以就手动复制下内容到~/.screenrc文件吧。

Linux vi/vim工具

功能介绍

所有的 Unix Like 系统都会内建 vi 文书编辑器,其他的文书编辑器则不一定会存在。vim 具有程序编辑的能力,可以主动的以字体颜色辨别语法的正确性,方便程序设计。

vi/vim使用

基本上 vi/vim 共分为三种模式,分别是命令模式(Command mode)输入模式(Insert mode)底线命令模式(Last line mode)

命令模式

220224-Linux实用工具-1.jpg

输入 作用
yy 复制游标所在的那一行。(常用)
dd 剪切游标所在的那一整行(常用),用 p/P 可以粘贴。
p/P p粘贴在光标行下面,P粘贴在光标行上面。
u 复原前一个动作。(常用)
[Ctrl]+r 重做上一个动作。(常用)
/word 向光标之下寻找一个名称为 word 的字符串。 (常用)
?word 向光标之上寻找一个字符串名称为 word 的字符串。
i, I 进入输入模式(Insert mode)
ZZ 如果修改过,保存当前文件,然后退出!效果等同于保存并退出

常用的还有替换功能:

  • :1,$s/word1/word2/g:%s/word1/word2/g,从第一行到最后一行寻找 word1 字符串,并将该字符串取代为 word2 !(常用)
  • :1,$s/word1/word2/gc:%s/word1/word2/gc,从第一行到最后一行寻找 word1 字符串,并将该字符串取代为 word2 ,加了c表示取代前显示提示字符给用户确认 (confirm) 是否需要取代。

输入模式

输入模式比较简单,可以使用键盘上的Home、End等功能键,按Esc退出输入模式到命令模式

底线命令模式

输入 作用
:w 将编辑的数据写入硬盘档案中(常用)
:q 离开 vi (常用)
:q! 若曾修改过档案,又不想储存,使用 ! 为『强制』离开不储存档案。
:wq 储存后离开,若为 :wq! 则为强制储存后离开 (常用)
:w [filename] 将编辑的数据储存成另一个档案(类似另存新档)
:r [filename] 在编辑的数据中,读入另一个档案的数据。亦即将 『filename』 这个档案内容加到游标所在行后面
:n1,n2 w [filename] 将 n1 到 n2 的内容储存成 filename 这个档案。
:! command 暂时离开 vi 到指令行模式下执行 command 的显示结果!例如
『:! ls /home』即可在 vi 当中察看 /home 底下以 ls 输出的档案信息!
:set nu/nonu 显示/取消行号

Linux tldr工具

功能介绍

一个比 –help 和 man 好用的查指令手册的工具,点我跳到tldr(too long don’t read)主页

安装

我使用sudo apt install tldr安装,然后mkdir -p ~/.tldr/tldr,再更新字典sudo git clone https://gitclone.com/github.com/tldr-pages/tldr.git ~/.tldr/tldr,国内使用这个镜像快。

推荐:官网上说可以使用npm或pip3安装。

使用

使用方式极其简单,tldr tldr你就可以查到tldr的使用手册。

Linux Samba工具

功能介绍

samba 是基于SMB协议(ServerMessage Block,信息服务块)的开源软件,samba也可以是SMB协议的商标。SMB是一种Linux、UNIX系统上可用于共享文件和打印机等资源的协议,这种协议是基于Client\Server型的协议,Client端可以通过SMB访问到Server(服务器)上的共享资源。当Windows是 Client,Ubuntu是服务器时,通过Samba就可以实现window访问Linux的资源,实现两个系统间的数据交互。samba服务程序已经成为在Linux系统和Windows系统之间共享文件的最佳选择,当然在Linux系统与Linux系统之间的文件共享也选择samba。

安装

在Ubuntu中安装sudo apt install samba,会自动安装其依赖组件。

安装后可用samba -V查看samba版本号,以确认安装完成。

使用

配置文件目录在/etc/samba/smb.conf,配置以下内容可共享home目录下用户文件夹。

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
# Un-comment the following (and tweak the other settings below to suit)
# to enable the default home directory shares. This will share each
# user's home directory as \\server\username
[homes]
comment = Home Directories
browseable = no

# By default, the home directories are exported read-only. Change the
# next parameter to 'no' if you want to be able to write to them.
read only = no

# File creation mask is set to 0700 for security reasons. If you want to
# create files with group=rw permissions, set next parameter to 0775.
create mask = 0775

# Directory creation mask is set to 0700 for security reasons. If you want to
# create dirs. with group=rw permissions, set next parameter to 0775.
directory mask = 0775

# By default, \\server\username shares can be connected to by anyone
# with access to the samba server.
# Un-comment the following parameter to make sure that only "username"
# can connect to \\server\username
# This might need tweaking when using external authentication schemes
valid users = %S

遇到的问题

以上配置后需要重启samba服务生效,可以注销用户后重新登录。

通过sudo smbpasswd -a userName添加一个samba用户,不然直接windows远程访问会拒绝访问。

userName需要是系统已有的用户名,否则会Failed to add entry for user userName

再次了解进程与线程

  • Python异步与线程:这是21年6月时对线程、进程、同步、异步的简单了解,最近在做毕设时又用到了,打算重新回顾一遍。

多线程与多进程的区别

多线程 threading: 一个人有与异性聊天和看剧两件事要做。单线程的她可以看完剧再去聊天,但这样子可能就没人陪她聊天了「哼,发消息不回」。我们把她看成一个CPU核心,为她开起多线程——先看一会剧,偶尔看看新消息,在两件事(线程)间来回切换。多线程:单个CPU核心可以同时做几件事,不至于卡在某一步傻等着。

用处:爬取网站信息(爬虫),等待多个用户输入

多进程 processing: 一个人有很多砖需要搬,他领取手套、推车各种物资(向系统申请了资源)然后开始搬砖。然而他身边有很多人,我们让这些人去帮他!(一核有难,八核围观)。于是他们做了分工,砖很快就搬完了。多进程让多个CPU核心可以一起做事,不至于只有一人干活而其他人傻站着。

用处:进行高性能计算。只有多进程方案设计合理,才能加速计算。

Python中应用多进程

  • 毕设的上位机要接收显示图像,我在udp数据接收解码后,写入IO流,通过管道发给另一个进程,另一个进程做图像的处理与显示,这样就不会因为处理时间过长而阻塞下一次接收解码了。
  • multiprocessing — 基于进程的并行:multiprocessing是Python自带的多进程库,这是Python官方的文档对multiprocessing库的介绍。
  • Python的线程是操作系统线程,因此要有Python全局解释器锁。一个python解释器进程内有一条主线程,以及多条用户程序的执行线程。即使在多核CPU平台上,由于GIL的存在,所以禁止多线程的并行执行。Python 3.6 才让multiprocessing逐渐发展成一个能用的Python内置多进程库,可以进行进程间的通信,以及有限的内存共享。

两种多进程创建方式

Python多进程可以选择两种创建进程的方式,spawn与fork,实际使用中可以根据子进程具体做什么来选取用fork还是spawn。

  1. fork:除了必要的启动资源外,其他变量,包,数据等都继承自父进程,并且是copy-on-write的,也就是共享了父进程的一些内存页,因此启动较快,但是由于大部分都用的父进程数据,所以是不安全的进程
  2. spawn:从头构建一个子进程,父进程的数据等拷贝到子进程空间内,拥有自己的Python解释器,所以需要重新加载一遍父进程的包,因此启动较慢,由于数据都是自己的,安全性较高
1
2
multiprocessing.set_start_method('spawn')  # default on WinOS or MacOS
multiprocessing.set_start_method('fork') # default on Linux (UnixOS)

四种进程间通信方式

Python中进程间通信可以采用进程池Pool、管道Pipe、队列Queue,在新版本Python中多了共享内存Manager的方式。

  1. 进程池Pool:不怎么使用?通常使用另外两个方式通信
  2. 管道和队列:Queue用于多个进程间实现通信,Pipe是两个进程的通信。Queue通过put和get方法插入读取队列,Pipe通过send和recv方法发送和接收信息,用法可以参考这篇文章:python进程间通信,如果追求运行更快,那么最好使用管道Pipe而队列Queue,详细查看Python pipes and queues performance
  3. 共享内存Manager:Pipe Queue 把需要通信的信息从内存里深拷贝了一份给其他线程使用(需要分发的线程越多,其占用的内存越多)。而共享内存会由解释器负责维护一块共享内存(而不用深拷贝),这块内存每个进程都能读取到,读写的时候遵守管理(因此不要以为用了共享内存就一定变快)

编程中要注意

为了避免自己调用自己时重复执行主进程,**多进程的主进程一定要写在程序入口if __name__ == ‘__main__’:,否则可能会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
def function1(id):  # 这里是子进程
print(f'id {id}')

def run__process(): # 这里是主进程
from multiprocessing import Process
process = [mp.Process(target=function1, args=(1,)),
mp.Process(target=function1, args=(2,)), ]
[p.start() for p in process] # 开启了两个进程
[p.join() for p in process] # 等待两个进程依次结束

# run__process() # 主线程不建议写在 if外部。由于这里的例子很简单,你强行这么做可能不会报错
if __name__ =='__main__':
run__process() # 正确做法:主线程只能写在 if内部

设计高性能的多进程时,遵守以下规则:

  • 尽可能少传一点数据
  • 尽可能减少主线程的负担
  • 尽可能不让某个进程傻等着
  • 尽可能减少进程间通信的频率

网上一些好的文档

大学回忆录

大一上学期

回忆从开学前几日的寝室生活开始,室友都非常的好相处,大家在一起能玩的很嗨。再到大一我参加了两个学生会的部门(学生会办公室&党员之家实践部)和一个社团(单车俱乐部),自己在部门里认识了些不同班级不同专业的朋友,也交到了一位知心,大一上学期我天天晚上会和他去健身房,或是去钱塘江边散步,或是在寝室喝酒,元旦跨年我去了他家里,人生中第一次和别人跨年。参与了学生会的活动演出和演讲还有优秀干部的竞选;

大一寒假

寒假里我一个人坐飞机去了山东蓬莱,找我高考结束后在华为上班认识的同事,蓬莱待了7天,青岛待了2天。独自旅行,去一个陌生的地方,享受孤独,思考人生,一个人狂欢。旅行结束回家后,我学习了郭天祥的十天学会51单片机,但并没有完全学会;

大一下学期

大一下学期有我喜欢的课程模电,我通过教同学们解题来学习这门课程,因此也交到了陪伴我至今的女朋友。除了模电课我还选了一门电子课程设计也非常喜欢,老师授课讲了51单片机的知识,课程做了51数字钟(我在5月19日晚上写完,拍了个51开发版显示520倒计时的视频),最后的作业是51小车。这学期学生会部门成员积极性变差了,我最后没有选择留任执委,选择了留在电子创新实验室(一待就是三年);

大一暑假

暑假我留在实验室准备19年的全国大学生电子设计竞赛,暑假做了历年的题目(声音存储录放、风力摆、旋转倒立摆),那时候的我不知道熬夜会伤身体,每天白天调试,晚上在寝室看文档到凌晨2点。暑假里由于比较专注比赛,忘记关心女朋友,两人有过多次争执,女朋友不回消息,我直接去她家里了,就这样被她家人认识,至今我还常去她家蹭饭。这个暑假差一点点就分手了;

大二上学期

大二上学期,一开学和徐高东学长参加了工程训练大赛,那次比赛后感觉之前所有学过的东西都能联会贯通了。比赛的时候出现了意外,没有取得好成绩,东哥说了句:比赛不就是这样。随后我参加了飞思卡尔智能车竞赛,在校内训练的时候感觉都良好,本来计划我和洪晨益写程序,由于硬件画的板子实在惨不忍睹,就自己动手把硬件做完了,我那写代码的队友也很强,一人能维护好代码,那年寒假前我们俩调车调到学校关门。这学期,我和我的女朋友分手了,但几个星期又复合了,我们相互都很缺时间在一起交流;

大二寒假

那年是全世界肺炎流行的一年,寒假在家3个月,当然我也没闲着,趁家里无聊,打开电脑学了些东西,有python,tensorflow,sklearn,百度飞桨,git等,有潘利斌陪我一起学。疫情原因只能在家调飞卡,在家里把Altium Designer了一遍,画板子是没有问题了。3月在家网课,网课非常适合我,我可以不仅可以回放自己学校老师上课讲的,还能去B站慕课上找名校老师上课的视频;

大二下学期

中国疫情控制的很厉害,4月中就能返校了。这学期在学校主要是准备电赛,准备飞卡,飞卡参加的是AI组别,通过机器学习,训练后部署到单片机,实现自主规划路线;

大二暑假

暑假培训大一电赛新生,不过今年比赛延期了,暑假没有比赛。飞卡如期举行,离国赛就差0.02秒,怎么就这么可惜呀。电赛准备了很多模块,把模块间通信学的明明白白的,还有很多PCB上的设计规范也学了一遍,那年我们实验室都在看长江大学唐老师的教学视频。

大三上学期

大三上学期开始在黄道麒公司实习,老板非常厉害,抓住了疫情这个风口,做红外热像仪体温枪这些生意。公司离学校很近,我每天上完课就去公司,那段时间进步非常快,学的东西也非常多非常杂。用了好多品牌的国产mcu,用QT写了上位机,学习了二元光学。十月份浙江省电子大赛,作品实物验收满分取得了省二等奖,队友配合的非常棒!这学期上了算法与数据结构和数字逻辑设计、51汇编,非常感兴趣,买了块FPGA开发板练手;

大三寒假

寒假里我还在公司上班,老板让我住宾馆可以报销。寒假里我学习了linux应用开发,opencv,makefile,cmake,用树莓派做了个红外热像仪的demo。这个寒假我在公司借了很多书看,有linux的,opencv的,无线传感器网络的,感测技术的。在公司里和老板经常聊到很晚,我们谈未来的方向,谈生活时政等等,无所不谈,是一家有温度的公司

大三下学期

这学期了解了一些网络安全,开关电源设计,搭建了blog,仍然在公司实习。搞的东西太杂了容易忘记自己学过什么,写写博客记录下的话翻到还能有印象。公司里主要是把之前用树莓派跑的demo移到了m4的mcu,另一个项目是用高云fpga做了ov2640的串并转换到mcu处理,练习了fpga。5月份之前飞卡的软件队友提出来要再参加一次,让我做一下硬件,这次我非常熟练,PCB最多就设计了两版就完成了硬件设计;

大三暑假

暑假去了一家做半导体芯片的公司实习,是属于系统集成部,公司平均年龄比较大,部门里同事都不怎么说话,公司氛围不太好,领导经常会骂别人很凶,我一看不对劲就溜溜球了。暑假在做wifi图传的项目,尝试用m4自己看数据手册写WiFi驱动,不过后来这事没成,换了esp32用idf开发。很早开始我就对网络感兴趣,可能未来也会多往这个方向发展;

大四上学期

大四第一学期在世界五百强博世上班,公司的氛围特别好,领导都很有管理的能力,同事之间办公都非常的舒心,博世是个不加班的外企,但是工作效率挺高的,流程虽多,但处理速度很快。我在博世工作了4个月,学习了高压交流的PCB设计,直流无刷电机,永磁同步电机,把电力这一块学了一遍,也了解了电机的控制算法,最简单的六步换向,还有FOC。可以说这次实习经历相当难忘,每个牛的公司,都有一群好的团队管理者。这学期,还报名考研了,算是把大学的知识复习了一下,没考好,以后也不会再有考研的想法了;

还有最后一个假期

最后一个假期也没选择安逸,找了家做安防的公司实习,在这里我学到了linux驱动开发的知识,我的主管年龄挺大的技术很好,整个框架是他从头写出来的,我看git log一步步看他怎么写出来,太强了!面试的时候就被他的技术所吸引,这么强的人为人还非常谦虚,值得我去学习!在这里我重学了一年前了解过的makefile编写,shell脚本编程,C代码规范等等。看了主管写的几万行脚本代码,我再感叹到太强了!过年在家里自己系统的学了一遍计算机网络,本来还想学计算机组成原理和操作系统的,学操作系统的时候卡住了,看早期的linux源码也非常的费劲,只能推到我2月实习完回学校再学了;

致青春:如果没有源自内心的冲动,怎么可能登峰造极呢?