0%

起因

这几天在做云途单片机的flash接口移植,同时陀螺仪的代码也需要将校准参数保存到flash每次开机读取,于是就看了s32k单片机的flash操作和模拟eeprom,同时看了云途的手册和sdk,也看了stm32闪存编程参考手册和hal库。

参考资料

  1. 【正点原子】 手把手教你学STM32入门教学视频单片机 嵌入式 之 F103-基于新战舰V3/精英/MINI板
  2. 《STM32F10xxx闪存编程参考手册》,《STM32 FLASH 模拟 EEPROM 使用和优化》
  3. 云途、s32k的参考手册,云途和stm32的sdk
  4. nor flash原理详细讲解
  5. NANDFlash原理

eeprom, norflash, nandflash特性

搬运自EEPROM, NAND FLASH, NOR FLASH

内部结构

EEPROM基于浮栅管单元(Floating gate transister)的结构。 EEPROM 的 单元是由FLOTOX(Floating- gate tuneling oxide transister)及一个附加的Transister 组成。由于FLOTOX 的特性及两管结构,所以可以单bit写。它的每个存储单元并联。

flash属于广义的EEPROM。FLASH 基于EEPROM, 与EEPROM主要的不同是FLASH 去除了 EEPROM 存储单元里的Tansister, 简化了存储电路(注意不是控制电路)。除此之外FLASH 的浮栅工艺不同, 所以写入速度更快。

NOR FLASH的每个存储单元(bit)以并联的方式连接到数据bit线,方便对每一位进行随机存取。同时具有专用的地址线(可以理解为字线),可以实现byte的直接寻址。NORFLASH具有足够的地址和数据线来映射整个存储区域,类似于SRAM的工作方式。例如,具有16位数据总线的2Gbit(256MB)NOR闪存将具有27条地址线,可以对任何byte进行随机读取访问。所以NORFLASH可以按byte读取。

NAND FLASH各存储单元(bit)之间是串联的。在读取数据时,当字线和位线锁定某个晶体管时,该晶体管的控制极不加偏置电压,其它的 7 个都加上偏置电压而导通,如果这个晶体管的浮栅中有电荷就会导通使位线为低电平,读出的数就是 0,反之就是 1。所以每次读取都是读取一行bytes里面的同一个bit, 最后整合为一个块:它是按块读取。

成本,容量

EEPROM存储电路并联, 每个bit的存储单元还要多加一个三极管, 最复杂所以单位成本也最高(注意不是控制电路)
因此它容量最低。EEPROM都是几十kbytes到几百kbytes,基本没有有超过512K。

NOR FLASH去除了EEPROM存储单元的三极管,每个存储单元并联, 去除了EEPROM存储单元的三极管, 集成度较小, 所以单位成本高。容量一般为1~16MB。

NAND FLASH, 去除了EEPROM存储单元的三极管, 各存储单元之间串联, 所以集成度大,单位成本最低。容量一般为8~512MB。

写入

EEPROM和FLASH在写入新数据之前,必须先将一个单元擦除(写 1)。 然后再在相应位写0。

NOR&NAND FLASH 去除了EEPROM存储单元的三极管。 所以只能整块的擦除。每次擦除只能擦除一行字节的一个bit。这是一个降低了单位成本的折衷办法,因为这容易导致损耗:减少了总擦除/写入次数。擦除块越小擦除越快。 然而擦除块越小芯片面积和存储器成本增加。 与NOR闪存相比NAND闪存可以更经济高效地支持更小的擦除块。目前,NAND闪存的典型块大小为8KB至32KB, NOR Flash为64KB至256KB。

除此之外在NOR闪存中,每个字节在擦除之前都需要写入“0”。这使得NOR闪存的擦除操作比NAND闪存慢得多。例如,NAND闪存S34ML04G2需要3。5ms才能擦除128KB块,而NOR闪存S70GL02GT则需要约520ms来擦除类似的128KB扇区。这相差近150倍。除此之外NOR的擦除块更大,这就更慢了。

EEPROM和FLASH的浮栅工艺不同, 所以NANDFLASH写入速度最快, EEPROM居中, NOR最慢。

读取数据粒度与速度

EEPROM可以单字节读取。EERPOM一般用于低端产品,读的速度不需要那么快,真要做的话,其实也是可以做的和FLASH 差不多。它的每个存储单元并联。 所以它的特点是可以随机访问和修改任何一个字节,可以往每个bit中写入0或者1。

NOR FLASH可以单字节读取。NORFLASH的每个存储单元(bit)以并联的方式连接到数据bit线,方便对每一位进行随机存取;同时具有专用的地址线(可以理解为字线),可以实现byte的直接寻址。NORFLASH具有足够的地址和数据线来映射整个存储区域,类似于SRAM的工作方式。例如,具有16位数据总线的2Gbit(256MB)NOR闪存将具有27条地址线,可以对任何byte进行随机读取访问。所以NORFLASH可以按byte读取, 随机读取(给一个地址读一个字节)时间很短。

NAND FLASH以页为单位读取。NAND FLASH各存储单元(bit)之间是串联的。在读取数据时,当字线和位线锁定某个晶体管时,该晶体管的控制极不加偏置电压,其它的 7 个都加上偏置电压而导通,如果这个晶体管的浮栅中有电荷就会导通使位线为低电平,读出的数就是 0,反之就是 1。所以每次读取都是读取一行bytes里面的同一个bit, 最后整合为一个页:它是按页读取。 随机读取时间很长。NAND 的全部存储单元分为若干个块,每个块又分为若干个页,每个页是 512byte: 每个页有 512 条位线,每条位线下有 8 个存储单元。

NAND FLASH每页存储的数据正好跟硬盘的一个块存储的数据相同,这是设计时为了方便与磁盘进行数据交换而特意安排的。在NAND闪存中,使用多路复用地址和数据总线访问存储器。典型的NAND闪存使用8位或16位多路复用地址/数据总线以及其他信号,如芯片使能,写使能,读使能,地址锁存使能,命令锁存使能和就绪/忙碌。 NAND Flash需要提供命令(读,写或擦除),然后是地址和数据。这些额外的操作也使NAND闪存的随机读取速度慢得多。

NAND闪存的顺序访问持续时间通常低于NOR闪存设备中的随机访问持续时间。利用NOR Flash的随机访问架构,需要在每个读取周期切换地址线,从而累积随机访问以进行顺序读取。随着要读取的数据块的大小增加,NOR闪存中的累积延迟变得大于NAND闪存。因此,NAND Flash顺序读取可以更快。但是由于NAND Flash的初始读取访问持续时间要长得多,两者的性能差异只有在传输大数据块(超过1 KB)时才明显。

可靠性

EEPROM存储电路复杂, 具有较高的可靠性, 最稳定可以保存100年。

NOR FLASH存储电路比复杂, 具有比较高的可靠性。

NAND FLASH存储电路简单, 可靠性比较低。

闪存会遭遇称为位翻转的现象,其中一些位可以被反转。这种现象在NAND存储电路简单中所以位翻转比NOR更常见。NAND器件中的坏块是随机分布的。以前也曾有过消除坏块的努力,但发现成品率太低,代价太高,根本不划算。NAND需要对介质进行初始化扫描以发现坏块,并将坏块标记为不可用。一般来说NAND的Block0是没有位翻转的。随着擦除和编程周期在NAND闪存的整个生命周期中持续,更多的存储器单元变坏。因此坏块处理(错误探测/错误更正(EDC/ECC)算法。)是NAND闪存的强制性功能。 这导致NAND的控制器电路复杂。这个问题对于用NAND存储多媒体信息时不是致命的。 但如果用本地存储设备来存储操作系统,配置文件或其他敏感信息时,必须使用EDC/ECC系统以确保可靠性。

另一方面, NOR闪存坏块少,在存储器的使用寿命期间具有非常低的坏块累积。因此,当涉及存储数据的可靠性时,NOR Flash优于NAND Flash。可靠性的另一个方面是数据保留,这方面,NOR Flash再次占据优势,例如,NOR Flash闪存S70GL02GT提供20年的数据保留,最高可达1K编程/擦除周期,NAND闪存S34ML04G2提供10年的典型数据保留。

擦除次数

EEPROM 每次只需要擦除一个字节所以不容易损耗,可以擦写100w次。

NAND闪存编程和擦除次数比NOR闪存好10倍。在NAND闪存中每个块的最大擦写次数是一百万次,NOR的擦写次数是十万次,NAND存储器除了块擦除次数优势, 典型的NAND块尺寸要比NOR器件小8倍, 每个NAND存储器块在给定的时间内的删除次数要少一些。编程和擦除的数量曾是一个需要考虑的重要特性。随着技术进步,这已不再适用,因为这两种存储器在这方面的性能已经很接近。例如,S70GL02GT NOR和S34ML04G2 NAND都支持100,000个编程 - 擦除次数。但是,由于NAND闪存中使用的块尺寸较小,因此每次操作都会擦除较小的区域, 与NOR Flash相比其整体寿命更长。

XIP(eXecute In Place)

eXecute In Place,即芯片内执行、就地执行,是指CPU直接从存储器中读取程序代码执行,而不用再读到内存中。应用程序可以直接在flash闪存内运行,不必再把代码读到系统RAM中。

flash内执行是指nor flash不需要初始化,可以直接在flash内执行代码。但往往只执行部分代码,比如初始化RAM。好处即是程序代码无需占用内存,减少内存的要求。

EEPROM&NORFLASH 可以像内存一样读任意地址(任意字节),可以XIP,当然这也要看接口是不是内存接口,如果不支持随机读取就一般不行,大部分NOR flash带有SRAM接口,有足够的地址引脚来寻址,可以很容易地存取其内部的每一个字节。可以非常直接地使用基于NOR的闪存,可以像其他存储器那样连接,并可以在上面直接运行代码。有的Norflash可以并行连接实现XIP,也可以串行通过SPI实现XIP。对于PC这需要相应的总线桥(内存控制器)芯片支持, Soc内部要集成相应桥芯片, 挂接到PCIE或者AMBA总线。 桥芯片的硬件逻辑会实现串并转换,总线仲裁,Cache结构,Burst等逻辑。

NANDFLASH 是按块读取,随机读取太慢所以不适合XIP当然这也要看接口是不是内存接口,如果不支持随机读取就一般不行,NAND使用复杂的I/O口来串行地存取数据。 一般是8个引脚用来传送控制,地址和数据信息 NANDFLASH只是不适合做XIP,但并不是不能做XIP,它坏块多,读取也太慢。比如EMMC启动就必须要把代码load到RAM里才能启动,你看到某些芯片支持EMMC启动,必定是有片内程序把EMMC的代码读到了RAM里。

驱动复杂程度

NAND更容易遇到位翻转, 驱动和控制电路要复杂的多。在使用NAND器件时,必须先写入驱动程序,才能继续执行其他操作。NAND在每一行上有CRC位标记,以及一些用于指示行是否为好的位。NAND处理芯片将管理CRC计算,允许在读取时纠正位错误,并管理坏行。

在USB驱动器中,这一切都由USB接口芯片处理。在SSD中,它由SATA(或其他接口模式)芯片处理,因此CRC错误和坏点映射对用户来说都是不可见的。这意味着在NAND器件上自始至终都必须进行虚拟映射。NAND内存的可靠运行还需要损耗均衡(就是把擦除平均到每个块上),损耗均衡也由USB或SSD控制器处理,因此用户也不可见。

用途

通常,NOR闪存是需要较低容量,快速随机读取访问和更高数据可靠性的应用的理想选择,例如代码执行。
NAND闪存则非常适用于需要更高内存容量和更快写入和擦除操作的数据存储等应用。

LINUX嵌入式系统多用一个小容量的nor flash存储引导代码,用一个大容量的nand flash存放文件系统和内核。

ADC过采样

用过不少模块中有ADC采样,如陀螺仪、磁罗盘、气压计、AFE中,都有ADC过采样率的配置,这些模块配置过采样率,输出的ADC量化位数没变,ADC值静态偏移量减小(信噪比提高),所以可以理解为多次采样取平均值。使用ADC过采样能提升精度,但是由于输出ADC值为多次采样平均,并不是单次采样时刻的值,AFE采电池电压的同时采电流,电压的值不是采电流那一时刻的瞬时值,导致SOC估算不准确。使用ADC过采样会提升采样次数,增大ADC的功耗。

下面来说说书里介绍的过采样。首先是结论:过采样率每提高4倍,可以提高ADC 1bit的有效分辨率

假设ADC过采样率为4,ADC会采集4次将值相加,这其实增加了2bits的有效分辨率,这个过程还需要降低采样,或者下抽,下抽是将四次采样累计值>>1,这么做除了降低数据量外,就是可以提高分辨率。

ADC降采样(减采样/下采样/down sampling)

降采样网上的资料比较少,一般是原有的采样率比实际信号的有效最高频率要高很多,就可以对采到的信号做低通滤波,除去高频干扰后向下抽样。

采样这一块还得向香农和奈奎斯特学习,香农-奈奎斯特采样定理,当一个信号被减采样时,必须满足采样定理以避免混叠。为了满足采样定理的要求,信号在进行减采样操作前,必须通过一个具有适当截止频率的低通滤波器。这个用于避免混叠的低通滤波器,称为抗混叠滤波器。

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