0%

GPS模块

商品链接:Beitian北天高精度GPS模块NEO-M8M陶瓷天线GPS北斗GLONASS三模GNSS授时BN-357

输出协议:NMEA-0183协议

NMEA-0183协议

<CR> 回车,(ASCII 13, \r)
<LF> 换行,(ASCII 10, \n)
hh 报文$到*之间数据的异或校验

RMC

Recommended Minimum Specific GPS/TRANSIT Data(RMC)推荐定位信息

报文:$GPRMC,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>,<9>,<10>,<11>,<12>*hh<CR><LF>
<1> UTC 时间,hhmmss.sss(时分秒)格式
<2> 定位状态,A=有效定位,V=无效定位
<3> 纬度ddmm.mmmm(度分)格式(前面的0也将被传输)
<4> 纬度半球N(北半球)或S(南半球)
<5> 经度dddmm.mmmm(度分)格式(前面的0也将被传输)
<6> 经度半球E(东经)或W(西经)
<7> 地面速率(000.0~999.9 节,前面的0 也将被传输)
<8> 地面航向(000.0~359.9 度,以真北为参考基准,前面的0 也将被传输)
<9> UTC 日期,ddmmyy(日月年)格式
<10> 磁偏角(000.0~180.0 度,前面的0 也将被传输)
<11> 磁偏角方向,E(东)或W(西)
<12> 模式指示(仅NMEA0183 3.00 版本输出,A=自主定位,D=差分,E=估算,N=数据无效)

GGA

Global Positioning System Fix Data(GGA)GPS 定位信息

报文:$GPGGA,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>,<9>,M,<10>,M,<11>,<12>*hh<CR><LF>
<1> UTC 时间,hhmmss.sss(时分秒)格式
<2> 纬度ddmm.mmmm(度分)格式(前面的0 也将被传输)
<3> 纬度半球N(北半球)或S(南半球)
<4> 经度dddmm.mmmm(度分)格式(前面的0 也将被传输)
<5> 经度半球E(东经)或W(西经)
<6> GPS 状态:0=未定位,1=非差分定位,2=差分定位,6=正在估算
<7> 正在使用解算位置的卫星数量(00~12)(前面的0 也将被传输)
<8> HDOP 水平精度因子(0.5~99.9)
<9> 海拔高度(-9999.9~99999.9)
<10> 地球椭球面相对大地水准面的高度
<11> 差分时间(从最近一次接收到差分信号开始的秒数,如果不是差分定位将为空
<12> 差分站ID 号0000~1023(前面的0 也将被传输,如果不是差分定位将为空)

GSA

GPS DOP and Active Satellites(GSA)当前卫星信息

报文:$GPGSA,<1>,<2>,<3>,<3>,<3>,<3>,<3>,<3>,<3>,<3>,<3>,<3>,<3>,<3>,<4>,<5>,<6>*hh<CR><LF>
<1> 模式,M=手动,A=自动
<2> 定位类型,1=没有定位,2=2D 定位,3=3D 定位
<3> PRN 码(伪随机噪声码),正在用于解算位置的卫星号(01~32,前面的0 也将被传输)
<4> PDOP 位置精度因子(0.5~99.9)
<5> HDOP 水平精度因子(0.5~99.9)
<6> VDOP 垂直精度因子(0.5~99.9)

GSV

报文:$GPGSV,<1>,<2>,<3>,<4>,<5>,<6>,<7>,…<4>,<5>,<6>,<7>*hh<CR><LF>

<1> GSV 语句的总数
<2> 本句GSV 的编号
<3> 可见卫星的总数(00~12,前面的0 也将被传输)
<4> PRN 码(伪随机噪声码)(01~32,前面的0 也将被传输)
<5> 卫星仰角(00~90 度,前面的0 也将被传输)
<6> 卫星方位角(000~359 度,前面的0 也将被传输)
<7> 信噪比(00~99dB,没有跟踪到卫星时为空,前面的0 也将被传输)
注:<4>,<5>,<6>,<7>信息将按照每颗卫星进行循环显示,每条GSV 语句最多可以显示4 颗卫星的信息。其他卫星信息将在下一序列的NMEA0183 语句中输出。

VTG

Track Made Good and Ground Speed(VTG)地面速度信息

报文:$GPVTG,<1>,T,<2>,M,<3>,N,<4>,K,<5>*hh<CR><LF>
<1> 以真北为参考基准的地面航向(000~359 度,前面的0 也将被传输)
<2> 以磁北为参考基准的地面航向(000~359 度,前面的0 也将被传输)
<3> 地面速率(000.0~999.9 节,前面的0 也将被传输)
<4> 地面速率(0000.0~1851.8 公里/小时,前面的0 也将被传输)
<5> 模式指示(仅NMEA0183 3.00 版本输出,A=自主定位,D=差分,E=估算,N=数据无效)

GLL

Geographic Position(GLL)定位地理信息

报文:$GPGLL,<1>,<2>,<3>,<4>,<5>,<6>,<7>*hh<CR><LF>
<1> 纬度ddmm.mmmm(度分)格式(前面的0 也将被传输)
<2> 纬度半球N(北半球)或S(南半球)
<3> 经度dddmm.mmmm(度分)格式(前面的0 也将被传输)
<4> 经度半球E(东经)或W(西经)
<5> UTC 时间,hhmmss(时分秒)格式
<6> 定位状态,A=有效定位,V=无效定位
<7> 模式指示(仅NMEA0183 3.00 版本输出,A=自主定位,D=差分,E=估算,N=数据无效)

str转int/float函数

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
int hexStr2Int(char *pStr)
{
int num = 0;
while(1)
{
if(*pStr >= '0' && *pStr <= '9')
{
num *= 16;
num += *pStr - '0';
pStr++;
}
else if(*pStr >= 'a' && *pStr <= 'f')
{
num *= 16;
num += *pStr - 'a' + 10;
pStr++;
}
else if(*pStr >= 'A' && *pStr <= 'F')
{
num *= 16;
num += *pStr - 'A' + 10;
pStr++;
}
else
{
break;
}
}
return num;
}

int decStr2Int(char *pStr)
{
int num = 0;
char isMinus = 0;
if(*pStr == '-')
{
isMinus = 1;
pStr++;
}
while(*pStr >= '0' && *pStr <= '9')
{
num *= 10;
num += *pStr - '0';
pStr++;
}
if(isMinus)
num = -num;
return num;
}

float str2Float(char *pStr)
{
int intNum = 0;
float num = 0;
char isMinus = 0;
intNum = decStr2Int(pStr);
if(*pStr == '-')
{
isMinus = 1;
pStr++;
}
while(*pStr >= '0' && *pStr <= '9')
{
pStr++;
}
if(*pStr == '.')
{
pStr++;
if(*pStr >= '0' && *pStr <= '9')
{
num = decStr2Int(pStr);
}
while(*pStr >= '0' && *pStr <= '9')
{
num /= 10.f;
pStr++;
}
}
if(isMinus)
num = intNum - num;
else
num = intNum + num;
return num;
}

查找char和异或校验函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
inline uint16_t findChar(uint8_t findValue, uint8_t *pStr, uint16_t size)
{
uint16_t i = 0;
for(i = 0; i < size; i++, pStr++)
{
if(*pStr == findValue)
return i;
}
return 0xffff;
}

inline uint8_t calcXorCheck(uint8_t *pStart, uint8_t *pEnd)
{
uint8_t ret = 0;
while(pStart < pEnd)
{
ret ^= *pStart;
pStart++;
}
return ret;
}

utcTime增加一秒函数

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
uint32_t gps_addSecondUtcTime(uint32_t now)
{
uint8_t hh, mm, ss;
ss = now%100;
if(ss < 59)
{
now++;
}
else
{
ss = 0;
mm = (now/100)%100;
hh = (now/10000)%100;
if(mm < 59)
{
mm++;
}
else
{
mm = 0;
if(hh < 23)
{
hh++;
}
else
{
hh = 0;
}
}
now = hh*10000 + mm*100 + ss;
}
return now;
}

解析函数

Github:https://github.com/hao0527/gps_data_parse

Windows串口接收代码

参考

实现

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
#include <Windows.h>

void* serial_openSerial(void* lpFileName, unsigned int baudRate, unsigned int dwInQueue, unsigned int dwOutQueue) {
HANDLE hComm = NULL;
DCB dcb;
COMMTIMEOUTS commTimeOuts;
hComm = CreateFile(lpFileName, //串口名称
GENERIC_READ | GENERIC_WRITE, //允许读和写
0, //独占方式
NULL, // 无安全属性,不可被子程序继承
OPEN_EXISTING, //创建文件的性质,打开而不是创建
0, // Non Overlapped I/O
NULL); // Null for Comm Devices
SetupComm(hComm, dwInQueue, dwOutQueue);
GetCommState(hComm, &dcb);
dcb.BaudRate = baudRate;
SetCommState(hComm, &dcb);
commTimeOuts.ReadIntervalTimeout = 0;
commTimeOuts.ReadTotalTimeoutMultiplier = 0;
commTimeOuts.ReadTotalTimeoutConstant = 0;
commTimeOuts.WriteTotalTimeoutMultiplier = 0;
commTimeOuts.WriteTotalTimeoutConstant = 0;
SetCommTimeouts(hComm, &commTimeOuts); // 配置Timeout参数(ms),0表示不Timeout
return hComm;
}

int serial_readLen(void* hComm, unsigned char* pBuff, unsigned int len, unsigned int* pLenRead) {
return ReadFile(hComm, pBuff, len, pLenRead, NULL);
}

int serial_closeSerial(void* hComm) {
return CloseHandle(hComm);
}

int serial_purgeSerial(void* hComm) {
return PurgeComm(hComm, PURGE_RXCLEAR | PURGE_TXCLEAR);
}

注意

Windows.h的api是正确返回非0

不同系统编译各数据类型所占内存空间大小

区别

类型 win32 win64 linux32 linux64
char 1 1 1 1
short 2 2 2 2
int 4 4 4 4
long 4 4 4 8
long long 8 8 8 8
float 4 4 4 4
double 8 8 8 8
void* 4 8 4 8

总结

  1. 指针所占空间看系统是16位、32位还是64位。
  2. win64把long编成4字节,linux64把long编成8字节。
  3. 在32位系统中,int和long都是4字节,取值范围相同。

使用DMA发送串口数据问题

  1. CubeMX生成的代码初始化DMA和UART顺序问题,应该先初始化DMA再UART,可以在CubeMX中调整生成初始化代码的顺序。2207031-CubeMX-1.jpg
  2. CubeMX生成的代码使用HAL_UART_Transmit_DMA()后需要手动将串口状态配置成空闲状态,可以在DMA传输完成中断中加(&huart1)->gState = HAL_UART_STATE_READY;

串口接收溢出后接收不到数据

  1. 产生问题的原因:超出接收size、在没接收的时候接收超过1个字节的数据。
  2. 解决方法参考:stm32cube,HAL库 HAL_UART_Receive_IT中断接收多个字符,串口溢出卡死问题
  3. 关闭检测Overrun功能,或者使用错误处理回调函数。

串口接收一帧不定长数据

  1. 可使用tm32f7xx_hal_uart_ex.h中的HAL_UARTEx_ReceiveToIdle()函数。

HAL库操作Flash

  1. 参考:基于STM32F407 HAL库的Flash编程操作 和 STM32F10xxx闪存编程参考手册
  2. 在每次擦除或编程前先要解锁Flash,在HAL库中,只需要调用stm32f1xx_hal_flash.h中的HAL_FLASH_Unlock()函数。
  3. 擦除时最小要以页为单位,传入的地址需要注意是否是页的起始地址。
  4. 编程时要注意4字节对齐,不同单片机可能不同。

起因

不喜欢用现成的账本app记账,喜欢在手机记事本里记账,没次统计花费总额都需要按计算器,比较麻烦也不确定会不会按错,所以用Python写个脚本算算总共花费多少,额外也可以统计些自己想知道的数据。

账本格式

1
2
3
4
7.8 两餐-29 电费-57 开箱-30 充气宝-160
7.9 一餐-10 充话费-54
7.10 两餐-33 鼠标脚垫-11 早餐包-24
7.11 两餐-22 出行-7 理发-13

脚本代码

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
import matplotlib.pyplot as plt

'''
costStruct = [ {'date': '7.1', 'item': ['两餐'], 'consume': [33]},
{'date': '7.2', 'item': ['两餐', '遮阳布', '出行'], 'consume': [25, 6, 13]},
...
]
'''
costStruct = []
with open(file='cost.txt', mode='r', encoding='utf-8') as fp:
costStrList = fp.readlines()

# 删除空白行
delNum = 0
for i in range(len(costStrList)):
j = i - delNum
costStrList[j] = costStrList[j].strip('\n')
if len(costStrList[j].replace(' ', '')) == 0:
delNum = delNum + 1
del(costStrList[j])
# print(costStrList)

for i in range(len(costStrList)):
costDic = {'date': '', 'item': [], 'consume': []}
strList = costStrList[i].split(' ')
costDic['date'] = strList[0]
for j in range(1, len(strList)):
cost = strList[j].split('-')
costDic['item'].append(cost[0])
costDic['consume'].append(int(cost[1]))
costStruct.append(costDic)
# print(costStruct)

# 计算总共花费
dayCost = []
dateStr = []
for i in range(len(costStruct)):
dayCost.append(sum(costStruct[i]['consume']))
dateStr.append(costStruct[i]['date'])
print(costStruct[i]['date'] + ' cost ¥' + str(dayCost[i]))
print('total cost ¥' + str(sum(dayCost)))

# 绘图
plt.figure(figsize=(3+0.25*len(dayCost), 8), dpi=100) # 自适应长度
plt.bar(dateStr, dayCost)
plt.xticks(rotation=45)
plt.title('total cost ' + str(sum(dayCost)) + ' yuan')
plt.xlabel('date')
plt.ylabel('consume')
plt.show()

Github

代码存放在https://github.com/hao0527/costSummary,以后有新的统计分析需求,会直接在我的Github更新。

RC振荡器

在振荡电路中的频率选择部分可以只用电阻和电容构成,这种只用电阻和电容构成的振荡器称为RC振荡器。RC振荡器需要起振电路,常用的正弦波荡电路有文氏桥振荡电路,要起振所以电路是正反馈,RC构成选频网络,两个二极管和R3构成稳幅电路。

2207010-RC与晶体振荡器-1.jpg

RC振荡器容易封装到芯片中,MCU内部的时钟一般就是RC振荡器。成本低、功耗小、电路板上无需外部晶振,这些都是RC振荡器的优点。

缺点:MCU的内部振荡电路对外界干扰很敏感,非常容易受到外界环境温度的影响。同时精度也低,下图是用F767内部和外部振荡器生成1Hz方波的区别。

2207010-RC与晶体振荡器-2.jpg

晶体振荡器

只要在晶体板级上施加交变电压,就会是晶片产生机械变形振动,此现象即所谓逆压电效应。当外加电压频率等于晶体谐振器的固有频率时,就会发生压电谐振,从而导致机械变形的振幅突然增大。一般而言,晶振的振荡频率比较稳定。但是价格稍微高点,还有用晶体振荡器一般还要接两个15-33pF起振电容。

有源晶振(Oscillator,晶振)只需要供电自身就能起振,无源晶振(Crystal,晶体)最高精度为5ppm,而有源晶振的精度则可以达到0.1ppm。有源晶振的信号电平是固定,所以需要选择好合适输出电平,灵活性较差。无源晶振单片机可以配置振荡输出电压。

有源晶振
无源晶振

STM32CubeMX中的时钟配置

STM32中的时钟配置

BYPASS Clock Source:使用有源晶振的话,则只需要给它加上电源,即可输出时钟到MCU的时钟输入端,绕过MCU的OSC模块,时钟直接供MCU使用。

Crystal/Ceramic Resonator:使用晶体的话,除了外部需要加上谐振电容(有些会加上MΩ的反馈电阻)之外,还需要MCU内部的OSC振荡电路辅助才能正常产生所需时钟。

好久不见,甚是想念

一个多月没写博客了,毕业后学习时间少了,白天忙公司的项目,偶尔晚上有空看看自己想学的资料,自己还在做个地质分析仪的项目,每周日会花一天的时间做。为自己加油,2022年我还要完成这块FPGA的学习,感谢那位支持我学这块开发板的人。

安装Vivado

  1. 下载vivado安装包,资料链接B盘:https://pan.baidu.com/s/1eM7Sx-RmeYFE1ht_RPqxhw 提取码:a8vu
  2. 解压安装包到无中文路径的目录下,否则会出现安装包无法打开的情况。打开安装包,我在安装选件的页面取消了K系列、V系列和Soc Zynq的选件,安装空间要70GB左右,因此我还买了个1T的固态。
  3. 激活只需要网上下载对应版本的激活licences,在激活页面load a licences即可。

软件操作

  1. Tools -> Settings -> Text Editor中选择编辑器,我选择的是notepad++,需要将编辑器路径加到系统环境变量。
  2. 创建PLL IP核:220708-XilinxFPGA点灯-2.jpg
  3. 功能仿真,RTL分析,综合,约束输入,设计实现都在左侧的Flow Navigator中。

流水灯

  1. New Project,芯片选择xc7a35tfgg484-2。220708-XilinxFPGA点灯-1.jpg
  2. Add Sources -> Create File,创建led_top.v文件。
  3. Vivado中打开文件会调用Notepad++编辑器,编写流水灯代码:220708-XilinxFPGA点灯-5.jpg
  4. 再功能仿真(可选),再综合、约束输入。
  5. 最后生成bit流下载到开发板:220708-XilinxFPGA点灯-4.jpg
  6. Xilinx的集成开发环境要比Altera的好用不少,就是编译速度较慢。

交叉编译简介

交叉编译,是一个和本地编译相对应的概念,交叉编译通俗地讲就是一种平台上编译出的程序能够运行在不同体系结构的平台上,比如在PC平台(X86 CPU)上编译出能运行在ARM CPU的程序。

使用交叉编译的原因

主要原因是:嵌入式系统中的资源太少。具体的解释就是:所要运行的目标环境中,各种资源,都相对有限,所以很难进行直接的本地编译。嵌入式开发板的CPU、RAM、Falsh等硬件资源相对比较紧张,在已经运行了嵌入式Linux的前提下,没法方便的进行本地编译。因为编译,开发,都需要相对比较多的CPU,内存,硬盘等资源,而嵌入式开发上的资源,只够嵌入式(Linux)系统运行的,没太多剩余的资源,供你本地编译。

交叉编译工具链组成

常用交叉编译工具有交叉编译器、交叉连接器、交叉解释器还有交叉ELF文件工具、交叉反汇编器等工具。交叉编译工具链主要由binutils、gcc和glibc三个部分组成。有时出于减小 libc 库大小的考虑,也可以用别的 c 库来代替 glibc,例如 uClibc、dietlibc 和 newlib。

编译器能将我们编写的语言转成计算机可以识别的机器语言,解释器能够执行用其他计算机语言编写的程序的系统软件,它是一种翻译程序,转换一行,运行一行,再转换一行,再运行一行。解释性语言:Python,JavaScript,编译性语言:Java,c,c++。

交叉工具链命名规则

交叉编译工具链的命名规则为:arch - vendor - os - (gnu)eabi

arch – 体系架构,如ARM,MIPS,表示该编译器用于编译哪个目标平台的程序
vendor – 工具链提供商,通常是把vendor写成体系架构的值,比如cortex_a8
os – 运行编译产生的程序的目标操作系统,一般用linux表示有操作系统,none表示裸系统,uboot编译无os
eabi – 嵌入式应用二进制接口(Embedded Application Binary Interface),abi是计算机上的

编译工具使用(持续更新)

交叉编译工具使用方法与本地编译工具链基本一样,只是命名不同。

gcc

Linux系统下的GCC编译器实际上是GNU编译工具链中的一款软件,可以用它来调用其他不同的工具进行诸如预处理、编译、汇编和链接这样的工作。gcc编译器从拿到一个c源文件到生成一个可执行程序,中间一共经历了四个步骤:

220522-交叉编译工具链-1.jpg

ld

ld是GNU操作系统上的连接器,把二进制文件连接成可执行文件。ELF文件可用于程序的链接,重定位目标文件。用于链接的ELF文件格式:

220522-交叉编译工具链-2.jpg

从编译和链接角度看ELF文件ELF头,每个ELF文件都必须存在一个ELF_Header,这里存放了很多重要的信息用来描述整个文件的组织,如:版本信息、入口信息、偏移信息等,程序执行也必须依靠其提供的信息。

段头表,存放的是所有不同段将在内存中的位置。代码段.text section,存放已编译程序的机器代码,一般是只读的。只读数据段.rodata section,此段的数据不可修改,存放常量。数据段.data section,存放已初始化的全局变量。.bss section,未初始化全局变量,仅是占位符,不占据任何实际磁盘空间,目标文件格式区分初始化和非初始化是为了空间效率。

符号表.symtab section,它存放在程序中定义和引用的函数和全局变量的信息。.text节的重定位信息.rel.txt section,用于重新修改代码段的指令中的地址信息。.data节的重定位信息.rel.data section,用于对被模块使用或定义的全局变量进行重定位的信息。调试用的符号表.debug section。.strtab section,包含symtab和debug节中符号及节名。.line section,存储调试的行号信息,描述源代码和机器码之间的对应关系。

ELF(Executable and Linkable Format)的完整描述,可以参考这个文档 - 这里

size

220522-交叉编译工具链-3.jpg

用于显示二进制文件各节的大小。

text段最终是存放在FLASH存储器中的,text段不仅包含函数,还有常量。

data段是用于初始化数据(全局/外部),既有初始化值的数据。

bss段包含着所有未初始化(或初始化值为0)的数据(全局/外部)。

dec(decimal的缩写,即十进制数)是text,data和bss的算术和。

objcopy

220522-交叉编译工具链-4.jpg

把一种目标文件中的内容复制到另一种类型的目标文件中。

objcopy -O ihex xxxxxx.elf xxxxxx.hex 将编译生成的elf文件转换为hex格式的文件。

objcopy -O srec xxxxxx.elf xxxxxx.srec 将编译生成的elf文件转换为srec格式的文件。

Git和SVN

公司里常用的两种版本控制工具:Git和SVN,两者最大的区别就是Git是分布式,SVN是集中式。集中式的版本控制系统都有一个单一的集中管理的服务器,保存所有文件的修订版本,而协同工作的人们都通过客户端连到这台服务器,取出最新的文件或者提交更新。 分布式的版本控制系统都有一个单一的集中管理的服务器,保存所有文件的修订版本,而协同工作的人们在自己本地也会创建一个库,用于保存自己的修改与提交,之后再将自己库提交至服务器库进行更新。

Subversion的特点概括起来主要由以下几条:

  • 每个版本库有唯一的URL(官方地址),每个用户都从这个地址获取代码和数据;
  • 获取代码的更新,也只能连接到这个唯一的版本库,同步以取得最新数据;
  • 提交必须有网络连接(非本地版本库);
  • 提交需要授权,如果没有写权限,提交会失败;
  • 提交并非每次都能够成功。如果有其他人先于你提交,会提示“改动基于过时的版本,先更新再提交”… 诸如此类;
  • 冲突解决是一个提交速度的竞赛:手快者,先提交,平安无事;手慢者,后提交,可能遇到麻烦的冲突解决;

Git具有以下特点:

  • Git中每个克隆(clone)的版本库都是平等的。你可以从任何一个版本库的克隆来创建属于你自己的版本库,同时你的版本库也可以作为源提供给他人,只要你愿意;
  • Git的每一次提取操作,实际上都是一次对代码仓库的完整备份;
  • 提交完全在本地完成,无须别人给你授权,你的版本库你作主,并且提交总是会成功;
  • 甚至基于旧版本的改动也可以成功提交,提交会基于旧的版本创建一个新的分支;
  • Git的提交不会被打断,直到你的工作完全满意了,PUSH给他人或者他人PULL你的版本库,合并会发生在PULL和PUSH过程中,不能自动解决的冲突会提示您手工完成;
  • 冲突解决不再像是SVN一样的提交竞赛,而是在需要的时候才进行合并和冲突解决;
  • Git 也可以模拟集中式的工作模式,Git版本库统一放在服务器中,Git 的集中式工作模式非常灵活;
  • 可以为 Git 版本库进行授权:谁能创建版本库,谁能向版本库PUSH,谁能够读取(克隆)版本库;
  • 团队的成员先将服务器的版本库克隆到本地;并经常的从服务器的版本库拉(PULL)最新的更新;
  • 团队的成员将自己的改动推(PUSH)到服务器的版本库中,当其他人和版本库同步(PULL)时,会自动获取改变;
  • 你完全可以在脱离Git服务器所在网络的情况下,如移动办公或出差时,照常使用代码库;
  • 你只需要在能够接入Git服务器所在网络时,PULL和PUSH即可完成和服务器同步以及提交;
  • Git提供 rebase 命令,可以让你的改动看起来是基于最新的代码实现的改动;
  • Git 有更多的工作模式可以选择,远非 Subversion可比;
  • Git 分支是指针指向某次提交,而 SVN 分支是拷贝的目录,这个特性使 Git 的分支切换非常迅速,且创建成本非常低;

Git基本概念和常用命令

  • 工作区:就是你在电脑里能看到的目录。
  • 暂存区:英文叫 stage 或 index。一般存放在 .git 目录下的 index 文件(.git/index)中,所以我们把暂存区有时也叫作索引(index)。
  • 版本库:工作区有一个隐藏目录 .git,这个不算工作区,而是 Git 的版本库,版本库可分为远程仓库和本地仓库。
    220512-Git和SVN-1.jpg

Git 常用的是以下 6 个命令:git clonegit pushgit addgit commitgit checkoutgit pull
220512-Git和SVN-2.jpg

git 创建仓库的命令:

命令 说明
git init 初始化仓库
git clone 拷贝一份远程仓库,也就是下载一个项目。

提交与修改的命令:

命令 说明
git add 添加文件到暂存区
git status 查看仓库当前的状态,显示有变更的文件。
git diff 比较文件的不同,即暂存区和工作区的差异。
git commit 提交暂存区到本地仓库。
git reset 回退版本。
git rm 删除工作区文件。
git mv 移动或重命名工作区文件。

提交日志的命令:

命令 说明
git log 查看历史提交记录
git blame 以列表形式查看指定文件的历史修改记录

远程操作的命令:

命令 说明
git remote 远程仓库操作
git fetch 从远程获取代码库
git pull 下载远程代码并合并
git push 上传远程代码并合并

Git 分支管理:https://www.runoob.com/git/git-branch.html
Git 查看提交历史:https://www.runoob.com/git/git-commit-history.html
Git 标签:https://www.runoob.com/git/git-tag.html
更多命令查看Git完整命令手册地址:http://git-scm.com/docs

Tortoise SVN

SVN教程:https://www.runoob.com/svn/svn-tutorial.html

TortoiseSVN 是 SVN(Subversion) 版本控制系统的一个免费开源客户端,可以超越时间的管理文件和目录。TortoiseSVN 使用教程:https://www.runoob.com/svn/tortoisesvn-intro.html

企业通常是使用TortoiseSVN提供的图像化界面操作,使用较简单。Windows的Git提供了GitGUI,也可以使用TortoiseGit,配置参数也有图形化界面,之前实习的一家公司就是 TortoiseGit 和 TortoiseSVN 。

版本管控工具分支管理

SVN分支管理策略

  • trunk(主干|主线|主分支):是用来做主方向开发的,新功能的开发应放在主线中,当模块开发完成后,需要修改,就用branch。
  • branches(分支):分支开发和主线开发是可以同时进行的,也就是并行开发,分支通常用于修复bug时使用。
  • tags (标记):用于标记某个可用的版本,可以标记已经上线发布的版本,也可以标记正在测试的版本,通常是只读的。

branch是用来做并行开发的,这里的并行是指和trunk进行比较。比如,3.0开发完成,这个时候要做一个tag,tag_release_3_0,然后基于这个tag做release,比如安装程序等。trunk进入3.1的开发,但是3.0发现了bug,那么就需要基于tag_release_3_0做一个branch,branch_bugfix_3_0,基于这个branch进行bugfix,等到bugfix结束,做一个tag,tag_release_3_0_1,然后,根据需要决定branch_bugfix_3_0是否并入trunk。

Git分支管理策略

Git Flow模型中定义了主分支和辅助分支两类分支。其中主分支用于组织与软件开发、部署相关的活动,辅助分支组织用于解决特定的问题而进行的各种开发活动。Git Flow开发模型从源代码管理角度对通常意义上的软件开发活动进行了约束,为软件开发提供了一个可供参考的管理模型。Git Flow开发模型让代码仓库保持整洁,让小组各个成员之间的开发相互隔离,能够有效避免处于开发状态中的代码相互影响而导致的效率低下和混乱。

Git Flow模型的特点是只有2个主干分支,Master和Develop分支:Master分支上只有稳定的生产版本,Develop分支用于集成。其中还涉及到HotFix分支。而其他还有三类分支:Feature分支用于开发人员各自开发;Release用于代码合并和集成;HotFix用于产品版本代码的紧急修订。

master分支通常只能从其它分支合并,不能在master分支直接修改。master分支上存放的是随时可供在生产环境中部署的代码(Production Ready state)。当开发活动到一定阶段,产生一份新的可供部署的代码时,master分支上的代码会被更新。同时,每一次更新,最好添加对应的版本号标签(TAG),所有在Master分支上的Commit应该打Tag。

develop分支是保持当前开发最新成果的分支,一般会在此分支上进行晚间构建(Nightly Build)并执行自动化测试。develop分支产生于master分支, 并长期存在。当一个版本功能开发完毕且通过测试功能稳定时,就会合并到master分支上,并打好带有相应版本号的tag。develop分支是主开发分支,包含所有要发布到下一个Release的代码,主要合并其它分支,比如Feature分支。

辅助分支是用于组织解决特定问题的各种软件开发活动的分支。辅助分支主要用于组织软件新功能的并行开发、简化新功能开发代码的跟踪、辅助完成版本发布工作以及对生产代码的缺陷进行紧急修复工作。辅助分支通常只会在有限的时间范围内存在。辅助分支包括用于开发新功能时所使用的feature分支,用于辅助版本发布的release分支,用于修正生产代码中的缺陷的hotfix分支。辅助分支都有固定的使用目的和分支操作限制。通过对分支的命名,定义了使用辅助分支的方法。

feature分支可以从develop分支派生。feature分支的命名可以使用除master,develop,release-*,hotfix-*之外的任何名称。feature分支(topic分支)通常在开发一项新的软件功能的时候使用,分支上的代码变更最终合并回develop分支或者干脆被抛弃掉(例如实验性且效果不好的代码变更)。一般而言,feature分支代码可以保存在开发者自己的代码库中而不强制提交到主代码库里。Feature分支开发完成后,必须合并回Develop分支,合并完分支后一般会删feature分支,但也可以保留。

release分支可以从develop分支派生。release分支是为发布新的产品版本而设计的。在release分支上的代码允许做测试、bug修改、准备发布版本所需的各项说明信息(版本号、发布时间、编译时间等)。通过在release分支上进行发布相关工作可以让develop分支空闲出来以接受新的feature分支上的代码提交,进入新的软件开发迭代周期。当develop分支上的代码已经包含了所有即将发布的版本中所计划包含的软件功能,并且已通过所有测试时,可以考虑准备创建release分支。而所有在当前即将发布的版本外的业务需求一定要确保不能混到release分支内(避免由此引入一些不可控的系统缺陷)。成功的派生release分支并被赋予版本号后,develop分支就可以为下一个版本服务。版本号的命名可以依据项目定义的版本号命名规则进行。发布Release分支时,合并Release到Master和Develop, 同时在Master分支上打个Tag记住Release版本号,然后就可以删除Release分支。

hotfix分支可以从master分支派生。hotfix分支是计划外创建的,可以产生一个新的可供在生产环境部署的软件版本。当生产环境中的软件遇到异常情况或者发现了严重到必须立即修复的软件缺陷时,就需要从master分支上指定的TAG版本派生hotfix分支来组织代码的紧急修复工作。优点是不会打断正在进行的develop分支的开发工作,能够让团队中负责新功能开发的人与负责代码紧急修复的人并行的开展工作。hotfix分支基于Master分支创建,开发完后需要合并回Master和Develop分支,同时在Master上打一个tag。

使用Git遇到的问题

Git tag

参考:GIT 中如何打标签(git tag)

220512-Git和SVN-3.jpg

Git show

给历史版本打标签时用的commit可以通过git show查看历史版本的hash。

220512-Git和SVN-4.jpg

Git log

查看提交日志,当你要修改历史提交前,你可以通过git log看看要修改第几次的提交。

220512-Git和SVN-5.jpg

Git rebase

通过git rebase可以修改历史的版本,参考:Git系列之修改历史提交信息,使用rebase指令后会在vi编辑器中选择要修改哪次提交,然后通过vi编辑器修改提交的内容。

注意:异常退出可能导致文件丢失,不要慌终端有提示如何恢复。

220512-Git和SVN-6.jpg

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协议传输语音数据。本设计还有更多功能待完善。