0%

Cache概述

在嵌入式系统中,缓存(Cache)是一种重要的存储器,位于CPU和主存储器(如SRAM、DRAM)之间。它用于存储经常访问的数据和指令,以减少访问主存的时间,从而提高系统的整体性能。缓存可以显著减少CPU等待数据的时间,提高系统的响应速度和处理能力。

Cache工作原理

缓存通过存储从主存储器中读取的常用数据或指令来加快访问速度。当CPU需要访问某个数据或指令时,会首先检查缓存。如果缓存中存在所需数据(称为缓存命中),则直接从缓存中读取。如果缓存中不存在(称为缓存未命中),则从主存储器中读取数据,并将其存储到缓存中,以备后续访问。

S32K344 Cache资源情况

NXP S32K344拥有8KB I-Cache 和 8KB D-Cache,从系统框图看这个芯片,对于锁步核的应用,两个核的Cache无法共用,而TCM内存是可以共用的,锁步情况下TCM内存容量是不锁步的2倍。在Memory table里还能看到存在672Bytes的I-Cache tag和800Bytes的D-Cache tag,缓存需要一种机制来管理哪些数据当前存储在缓存中,这就是tag的作用。S32K344的RAM和ROM都有ECC校验,Cache也包含了ECC校验,RAM和ROM每64位数据有8位是存ECC校验值,这意味着实际设计的Cache资源还要更大一些用于存ECC校验值。

Cache维护

在缓存管理中,无效化(Invalidate)和清除(Clean)是两个不同的操作。这两个操作帮助维持缓存一致性,确保系统性能和数据正确性。

无效化(Invalidate)

作用:使缓存中的数据无效,但不写回主存。
使用场景:当你确定缓存中的数据已经过期或不再需要时,可以无效化缓存来避免使用陈旧数据。

清除(Clean)

作用:将缓存中的数据写回主存,但不使数据无效。
使用场景:当需要确保缓存中的最新数据已经同步到主存时,可以进行清除操作,特别是在DMA操作前后。

实际使用问题

开启Cache后Flash读写擦

S32K344,MCAL的INFLS代码可以配置开启Mem Synchronize Cache选项用于确保缓存与Flash存储之间的数据一致性。

功能:在每次Flash高电压操作(写入、擦除)后,通过调用MCL(Memory Control Layer)缓存API函数来无效化缓存,以确保缓存与修改后的Flash存储同步。但也存在缺点,如果要无效化的区域大于缓存的一半,则整个缓存会被无效化。

在实际使用中如果开启Cache,没开这个选项部分代码擦写Flash不生效,写入异常

代码加载到RAM运行

S32K344,MCAL的INFLS代码可以配置开启Mem Clean Cache After Load Access Code选项用于确保加载到RAM中的Access Code函数与缓存一致。

如果该选项启用,在将Access Code函数加载到RAM后,清除缓存,将缓存数据写入实际RAM内存。此操作可确保缓存和RAM之间的数据同步,避免缓存数据不一致的问题。在实际使用中遇到了开启这个选项但没开Cache功能,有概率会在加载RAM代码缓存操作时卡死问题

DMA 搬运到 Cache 区域

在使用 DMA 将 UART 收到的数据搬运到 Cache 区域前,必须先 Invalidate 那片区域的 Cache。这是为了确保在 DMA 操作过程中,该区域的 Cache 中不会有未写入内存的脏数据。这些脏数据如果存在,可能会在 DMA 写入数据时被覆盖,导致数据不一致的问题。

虽然在 DMA 搬运前也可以使用 Clean 操作,但相较于 Invalidate 操作,效率较低。Clean 操作会将 Cache 中的脏数据写回到内存,而 Invalidate 直接使 Cache 无效,避免了写回的开销,因此通常更高效。

数据搬运完成后,也需要 Invalidate 那片区域的 Cache。这一步是为了防止在接下来的数据访问过程中,Cache 中的陈旧数据被直接读取,而不是从内存中读取 DMA 搬运来的新数据。

内存区域配置不使用Cache

不同MCU实现不一样,有使用MPU(内存保护单元),配置MPU区域属性,将指定内存区域设置为不可缓存,设置MPU的Region属性,使该区域不可缓存。也有配置MMU(内存管理单元),如果使用MMU,可以通过设置页表条目属性将内存区域设置为不可缓存。或者是芯片特定的寄存器,参考芯片手册进行配置。

S32K344如何配置目前还没了解到,在链接脚本中看到有一块 SRAM 没有被缓存。顺着链接脚本中定义的变量应该能查到程序中哪里配置的不缓存,也有可能那块区域就是不缓存区域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
MEMORY
{
int_pflash : ORIGIN = 0x00440000, LENGTH = 0x00394000 /* 4096KB - 176KB (sBAF + HSE) - 256KB(boot) = 3664KB*/
int_dflash : ORIGIN = 0x10000000, LENGTH = 0x00020000 /* 128KB */
int_itcm : ORIGIN = 0x00000000, LENGTH = 0x00010000 /* 64KB */
int_dtcm : ORIGIN = 0x20000000, LENGTH = 0x0001F000 /* 124KB */
int_stack_dtcm : ORIGIN = 0x2001F000, LENGTH = 0x00001000 /* 4KB */
int_sram : ORIGIN = 0x20400000, LENGTH = 0x0002FF00 /* 184KB, needs to include int_sram_fls_rsv */
int_sram_fls_rsv : ORIGIN = 0x2042FF00, LENGTH = 0x00000100
int_sram_no_cacheable : ORIGIN = 0x20430000, LENGTH = 0x0000FF00 /* 64KB, needs to include int_sram_results */
int_sram_results : ORIGIN = 0x2043FF00, LENGTH = 0x00000100
int_sram_shareable : ORIGIN = 0x20440000, LENGTH = 0x00004000 /* 16KB */
ram_rsvd2 : ORIGIN = 0x20444000, LENGTH = 0 /* End of SRAM */
}

LVGL介绍

LVGL 是最流行的免费和开源嵌入式图形库,可为任何 MCU 和 MPU 显示类型创建漂亮的 UI。

需求说明

这是去年做的一个小项目,手持热像仪,需要LCD显示摄像头的实时图像。之前做过直接用LCD驱动显示图片效率会高些,这一次需要加一下UI给用户选配置等功能,所以选择使用LVGL。做UI是小问题,主要是如何显示实时图像,本文将介绍使用图像解码器的方法。

Image decoder(图像解码器)

图像解码器原本被用来解码通用图像格式,如 PNG 或 JPG。我把摄像头读来数据通过图像解码器封装了一下,当做自定义格式去解码,解码器接口如下,最后注册到LVGL,主要实现了 get_info 和 read_line 这两个接口。

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
uint16_t img_ram[MY_IMG_H][MY_IMG_W];	// 摄像机读来的图像数据

static lv_res_t my_decoder_get_info(lv_img_decoder_t* decoder, const void* src, lv_img_header_t* header)
{
LV_UNUSED(decoder);
if(strcmp("use my_decoder", src))
{// 如果src 不等于 "use my_decoder",不使用此解码器
return LV_RES_INV;
}
header->w = MY_IMG_W;
header->h = MY_IMG_H;
header->always_zero = 0;
header->cf = LV_IMG_CF_TRUE_COLOR; // 使用lv_conf.h 中 LV_COLOR_DEPTH 配置的颜色
return LV_RES_OK;
}

static lv_res_t my_decoder_open(lv_img_decoder_t* decoder, lv_img_decoder_dsc_t* dsc)
{
LV_UNUSED(decoder);
LV_UNUSED(dsc);
return LV_RES_OK;
}

static lv_res_t my_decoder_read_line(lv_img_decoder_t* decoder, lv_img_decoder_dsc_t* dsc, lv_coord_t x, lv_coord_t y, lv_coord_t len, uint8_t* buf)
{
LV_UNUSED(decoder);
LV_UNUSED(dsc);
uint16_t* destBuff = (uint16_t*)buf;
uint16_t* srcBuff = (uint16_t*)img_ram;
uint32_t offset = x + y * MY_IMG_W;
for(int i = 0; i < len; i++)
{
destBuff[i] = srcBuff[offset + i];
}
return LV_RES_OK;
}

static void my_decoder_close(lv_img_decoder_t* decoder, lv_img_decoder_dsc_t* dsc)
{
LV_UNUSED(decoder);
LV_UNUSED(dsc);
return LV_RES_OK;
}

/**
* @brief 创建一个解码器,将自定义解码器的接口注册到此解码器
*/
void my_decoder_init(void)
{
lv_img_decoder_t* dec = lv_img_decoder_create();
lv_img_decoder_set_close_cb(dec, my_decoder_close);
lv_img_decoder_set_info_cb(dec, my_decoder_get_info);
lv_img_decoder_set_open_cb(dec, my_decoder_open);
lv_img_decoder_set_read_line_cb(dec, my_decoder_read_line);
}

定时刷新和 main 函数代码如下:

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
void my_timer(lv_timer_t * timer)
{
lv_obj_t * img = timer->user_data; // 取出定时器的userdata
// 这里需实现更新摄像机的图像数据到 img_ram 数组的代码
lv_obj_invalidate(img); // 将img对象标记为无效以重新绘制其区域
}

void my_show_img(void)
{
lv_obj_t * img = lv_img_create(lv_scr_act());
lv_img_set_src(img, "use my_decoder");
lv_obj_align(img, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_size(img, MY_IMG_W, MY_IMG_H);
lv_timer_create(my_timer, 100, (void*)img); // 创建定时器,周期100ms,定时器userdata设为img对象
}

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR szCmdLine, int nCmdShow)
{
/*Initialize LittlevGL*/
lv_init();

/*Initialize the HAL for LittlevGL*/
lv_win32_init(hInstance, SW_SHOWNORMAL, 800, 480, NULL);

/*Output prompt information to the console, you can also use printf() to print directly*/
LV_LOG_USER("LVGL initialization completed!");

/*Run the demo*/
my_decoder_init();
my_show_img();

while(!lv_win32_quit_signal) {
/* Periodically call the lv_task handler.
* It could be done in a timer interrupt or an OS task too.*/
lv_task_handler();
usleep(10000); /*Just to let the system breath*/
}
return 0;
}

其他方法

Canvas(画布)(lv_canvas)

命令提示符

命令提示符是在操作系统中,提示进行命令输入的一种工作提示符。在不同的操作系统环境下,命令提示符各不相同。在windows环境下,命令行程序为cmd.exe,是一个32位的命令行程序(在64位系统中 cmd.exe 也存在于 SysWOW64 目录,WOW64 是 x86 模拟器,允许基于 Windows 的 32 位应用程序在 64 位 Windows 上运行),微软Windows系统基于Windows上的命令解释程序,类似于微软的DOS操作系统(可惜,我这个年代的人都没见过DOS~)。

批处理文件

批处理文件是无格式的文本文件,它包含一条或多条命令。它的文件扩展名为 .bat 或 .cmd。在命令提示下输入批处理文件的名称,或者双击该批处理文件,系统就会调用 cmd.exe 按照该文件中各个命令出现的顺序来逐个运行它们。使用批处理文件(也被称为批处理程序或脚本),可以简化日常或重复性任务。

BAT实现些小功能

echo 和 @

Cmd中的echo命令通过echo /?可以看介绍。

用于显示消息:ECHO [message],(注意:echo不会去message引号)
启用或关闭命令回显:ECHO [ON | OFF],(提一句:Cmd命令不区分大小写)

还有个关回显的方式:@表示本条命令不回显,仅在本条命令生效,优先级高于echo off。

设变量

和Shell不同,Cmd中设变量需要加set,如set VariableName=100,等号两边不应有空格。设好变量后可用echo %VariableName%来打印刚才设置的变量。

如果要看系统所有的环境变量可以在Cmd中输入set就会打印到终端里,如果要保存到文件中可以set > Variable.txt>和Shell一样,可以将左边命令输出重定向到指定文件,用>>就是追加写入。

系统中有很多自带的环境变量,下面几个是我感觉可能会用到的:

  • %CD% 本地 返回当前目录字符串。
  • %USERNAME% 本地 返回当前登录的用户的名称。
  • %DATE% 系统 返回当前日期。使用与 date /t 命令相同的格式。
  • %ERRORLEVEL% 系统 返回上一条命令的错误代码。(通常用非零值表示错误)
  • %PATH% 系统 指定可执行文件的搜索路径。(有多个版本Gcc工具链可以通过设这个环境变量再指定,越在前优先级越高,可以把要用的加在前面)
  • %WINDIR% 系统 返回操作系统目录的位置。

值得思考:变量来自哪里?不同地方定义的环境变量哪个优先级高?

发现异常

用上面提到的ERRORLEVEL环境变量可以知道上一次命令的返回结果,成功为0(默认值),失败非0。可以根据这个变量去做命令执行失败的处理。

变长参数解析

配合shift命令,将参数一个个解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
echo Usage: build.bat [--relese ^<LOG_FILE^>] [make target ...]
:parse-args
rem 检测到没有下个参数,跳到编译
if "%1"=="" ( goto pre-build1 )
if "%1"=="--release" (
rem 解析到--release,还需再读一个后面跟的文件地址
set RELEASE_FLAG=1
set SVN_LOG_FILE=%RUN_PATH%\%2
shift
) else (
rem 其他参数都归自定义make参数
set CUSTOM_MAKE_ARGS=%CUSTOM_MAKE_ARGS% %1
)
rem shift更改参数的位置
shift
goto parse-args

传地址参数

脚本经常会遇到传地址参数,比如我的–release参数后面就要跟一个LOG_FILE的参数,需要传递文件路径。

传进来的相对参数正常是基于脚本启动路径,脚本里如果有切换执行路径,在使用地址参数时要加这个路径偏移。可以在执行脚本刚开始记录启动路径(绝对路径)set RUN_PATH=%cd%,后面在解析文件参数时加上启动路径set SVN_LOG_FILE=%RUN_PATH%\%2

这样可以解决相对路径的传参问题,但是绝对路径无法解决,Cmd脚本如何识别传入的路径是相对or绝对?

在Windows中,绝对路径组成有:驱动器号:\目录名\目录名\...\文件名,可以通过识别有无:来判断是否为绝对路径。

丢弃异常输出

在Windows命令行脚本中,> nul 是一种将命令输出重定向到空设备的方式。这通常用于抑制命令的输出,使其不显示在命令行窗口中。通常在删除时不报找不到文件错可以加,让异常输出不显示使用 2 > nul

1
2
3
4
5
6
7
set FILE_PATH=%1
echo %FILE_PATH% | find ":" > nul
if errorlevel 1 (
echo 传入的路径为相对路径
) else (
echo 传入的路径为绝对路径
)

延迟拓展

在cmd执行命令前会对脚本进行预处理,其中有一个过程是变量识别过程,在这个过程中,如果有两个%括起来的如%value%类似这样的变量,就会对其进行识别,并且查找这个变量对应的值,再而将值替换掉这个变量,这个替换值的过程,就叫做变量扩展,然后再执行命令。

1
2
3
@echo on
set a=4
set a=5& echo %a%

Cmd脚本会逐行预处理(用到括号会被当成一行,开@echo on可以看到),上面脚本在预处理时第三行会被变量拓展为set a=5& echo 4,所以就算设置了a=5,打印内容已经被拓展了还是4。

1
2
3
4
@echo on
setlocal enabledelayedexpansion
set a=4
set a=5& echo !a!

设置本地开启延时拓展:setlocal enabledelayedexpansion,设置本地关闭延时拓展:setlocal disabledelayedexpansion。这两个也相当于命令,需要运行到才会起作用。如果不开启延时拓展,使用到的延时拓展变量不会被拓展echo的内容为!a!

开启延时拓展后,要被延时拓展的变量要用一对叹号!value!括起来,上面脚本在预处理后为set a=5&echo !a!,在执行echo时才会解析变量a,打印的内容是5。

小技巧:可以开启延时拓展后使用echo %value% !value!看是否一致,如果一致可以不使用延时拓展该变量(最好还是理解原理)。

调子程序

子程序写法:

1
2
3
4
5
:getSum
set /a sum+=%1
shift /1
if not "%1"=="" goto getSum
goto :eof

调用方法:

1
2
3
set sum=0
call:getSum 1 2 3
echo %sum%

输出:6

脚本不用像C一样先声明后使用,子程序可以写在调用的下方。

条件语句

语法格式:if [not] <条件> (执行语句),注意:执行语句如果用括号,左括号和条件间必须有个空格。

多个条件与:if [not] <条件> [if [not] <条件> ...] (执行语句)

多个条件或(写法比较不优雅):

1
2
3
4
5
6
if [not] <条件> goto do-something
if [not] <条件> goto do-something
...
:do-something
执行语句
...

循环语句

Windows bat脚本的for语句基本形态如下:
在cmd窗口中:for %I in (command1) do command2
在批处理文件中:for %%I in (command1) do command2

for、in、do是for语句的关键字,它们三个缺一不可。%%I是for语句中对形式变量的引用,即使变量I在do后的语句中没有参与语句的执行,也是必须出现的。in之后,do之前的括号不能省略。command1表示字符串或变量,command2表示字符串、变量或命令语句。具体使用可以看下面的BAT解析 MarkdownTable。

脱引号(转载

很多情况下,我们需要脱除一个字符串中可能会存在的引号,然后在加上自己的引号使其中的特殊字符(命令连接符& 、| 、&&、||,命令行参数界定符Space 、tab 、 ; 、= ,字符化转义符^ 、” ,变量化转义符%等)字符化,失去特定的作用,而作为普通的字符成为字符串的一个组成部分。

如果字符串存在于命令行参数%1中,可以使用%~1脱去第一对外侧引号,如果没有外侧引号则字符串不变。

如果字符串存在于for替代变量%%i中,可以使用%%~i脱去第一对外侧引号,如果没有外侧引号则字符串不变。

如果字符串存在于环境变量%temp%中,可以使用%temp:"=%脱去其中所有的引号,如果没有引号则字符串不变。

BAT解析 MarkdownTable

解析 MarkdownTable 子程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
::==============================Parse Markdown Function============================
rem 定位Markdown中相应标题所在起始行和结束行
:locateMarkdownTitleFunc
echo Entering Function %0
echo inFileName %1
rem titleName需加引号
echo titleName %2

set startLineNum=
set endLineNum=

rem 查找所有#开头的标题行
for /F "tokens=*" %%i in ('findstr /i /n "^#" %1') do (
set "line=%%i"
rem 解析标题行的行号和内容
for /F "tokens=1* delims=:" %%j in ("!line!") do (
set linenum=%%j
set "linestr=%%k"
rem 已有起始行 没有结束行,设置结束行
if "!startLineNum!" neq "" if "!endLineNum!" equ "" (
set endLineNum=!linenum!
)
rem 去标题内容首部#和空格
for /F "tokens=* delims=#" %%a in ("!linestr!") do set "linestr=%%a"
for /F "tokens=* delims= " %%a in ("!linestr!") do set "linestr=%%a"
rem 标题名匹配 没有起始行,设置起始行(多个匹配使用第一个)
if /i "!linestr!" equ %2 if "!startLineNum!" equ "" (
set startLineNum=!linenum!
)
)
)
rem 有起始行没有结束行则取文件总行数+1作为结束行
if "%startLineNum%" neq "" if "!endLineNum!" equ "" (
for /F %%a in ('find /c /v "" ^< %1') do set /a endLineNum=%%a+1
)
echo Leaving Function %0
goto:eof

rem 解析README中的Repo Link表格,设环境变量
:parseRepoLinkFunc
echo Entering Function %0
echo inFileName %1
echo argsSuffix %2

rem 定位Markdown中Repo Link标题所在起始行和结束行
call:locateMarkdownTitleFunc %1 "Repo link"
set RepoLinkStartLineNum=%startLineNum%
set RepoLinkEndLineNum=%endLineNum%
echo RepoLinkStartLineNum=%RepoLinkStartLineNum%
echo RepoLinkEndLineNum=%RepoLinkEndLineNum%

rem 解析svn配置行
for /F "tokens=1,2,3,4 delims=^| " %%i in ('findstr /i /n "svn.*http" %1') do (
rem 解析行号
for /F "tokens=1* delims=:" %%a in ("%%i") do (
set linenum=%%a
)
rem 行号大于标题开始行 且 小于标题结束行 则解析,设环境变量
if !linenum! gtr %RepoLinkStartLineNum% if !linenum! lss %RepoLinkEndLineNum% (
set SvnUrlMd_%2=%%l
)
)

rem 解析git配置行
for /F "tokens=1,2,3,4 delims=^| " %%i in ('findstr /i /n "git.*http" %1') do (
rem 解析行号
for /F "tokens=1* delims=:" %%a in ("%%i") do (
set linenum=%%a
)
rem 行号大于标题开始行 且 小于标题结束行 则解析,设环境变量
if !linenum! gtr %RepoLinkStartLineNum% if !linenum! lss %RepoLinkEndLineNum% (
set GitUrlMd_%2=%%l
set BranchMd_%2=%%k
)
)

echo Leaving Function %0
goto:eof

rem 解析README中的Config表格,输出到文件
:parseConfigFunc
echo Entering Function %0
echo inFileName %1
echo outFileName %2

rem 清空outFile
del %2 2>nul

rem 定位Markdown中Config标题所在起始行和结束行
call:locateMarkdownTitleFunc %1 "Config"
set ConfigStartLineNum=%startLineNum%
set ConfigEndLineNum=%endLineNum%
echo ConfigStartLineNum=%ConfigStartLineNum%
echo ConfigEndLineNum=%ConfigEndLineNum%

rem 查找Config标题中Config Name所在行
for /F "tokens=*" %%i in ('findstr /i /n /c:"Config Name" %1') do (
for /F "tokens=1* delims=:" %%a in ("%%i") do (
set linenum=%%a
if !linenum! gtr %ConfigStartLineNum% if !linenum! lss %ConfigEndLineNum% (
rem 设置Config Table内容开始行
set /a tableStart=!linenum!+2
)
)
)

rem 逐行遍历Markdown文件
for /F "tokens=1,2,3,4 delims=^| " %%i in ('findstr /i /n "^" %1') do (
rem 解析行号
for /F "tokens=1* delims=:" %%a in ("%%i") do (
set linenum=%%a
)
rem 行号大于等于表格内容开始行则解析
if !linenum! geq %tableStart% (
set configName=%%j
set configValue=%%k
rem 检测为空判定为表格结束,退出函数
if "!configName!" equ "" ( goto leaveParseConfig )
if "!configValue!" equ "" ( goto leaveParseConfig )
rem 存到输出文件
echo !configName!=!configValue!>>%2
echo !configName!=!configValue!
rem 是PostSyncJob 则 设环境变量
if "!configName!" equ "PostSyncJob" (
set PostSyncJob=!configValue!
)
)
)
:leaveParseConfig
echo Leaving Function %0
goto:eof
::=================================================================================

使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
echo 进入git仓库, 解析README.md文件
call:parseRepoLinkFunc repo\repo_git\README.md Git
echo GitRepo README.md Info:
echo SvnUrlMd_Git = %SvnUrlMd_Git%
echo GitUrlMd_Git = %GitUrlMd_Git%
echo BranchMd_Git = %BranchMd_Git%

echo 进入svn仓库, 解析README.md文件
call:parseRepoLinkFunc repo\repo_svn\README.md Svn
echo SvnRepo README.md Info:
echo SvnUrlMd_Svn = %SvnUrlMd_Svn%
echo GitUrlMd_Svn = %GitUrlMd_Svn%
echo BranchMd_Svn = %BranchMd_Svn%

rem Config写入README_Config.txt
call:parseConfigFunc repo\repo_git\README.md README_Config.txt

结尾

这几天边写边学BAT脚本,刚接触BAT感觉和Python比起来不够可读和易用,和Shell比起来命令功能不强大。BAT的编程能力与C语言等编程语句比起来是十分有限的,也是十分不规范的。批处理的程序语句就是一条条的DOS命令(包括内部命令和外部命令),而批处理的能力主要取决于你所使用的命令,但在Windows平台上命令确实不如Linux上的好用,在Windows上装MinGW使用Linux上的命令又涉及到了字符转义和编码不同,bat中调Linux下的命令会非常乱,不推荐我这种菜鸡使用,不如直接全部上Linux。

好东西

Linux下命令有很多,不常用或者首次用比较陌生,好在可以使用tldr这个命令做提示,比命令自带的帮助文档精简。发现一个网页版的tldr,数据库还经常更新,tldr-inbrowser

前言

以前写脚本的时候用到过传参:python最简单,直接import argparse。shell中使用$#表示传递到脚本的参数个数,用$*表示以一个单字符串显示所有向脚本传递的参数,如”$*“用「”」括起来的情况、以”$1 $2 … $n”的形式输出所有参数。bat脚本传参和shell类似,%#表示传递到脚本的参数个数,%*表示参数字符串,*可以是数字。在C语言中可以使用getopt,它是一个标准的C库函数,用于解析命令行参数。它可以帮助你处理短选项(-h)和长选项(–help)。

今天学习 adpcm-xq 看到一个C代码直接用指针操作argv,如*++*argv,能差不多看懂,现在回家再来理一下。

代码片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
int main (argc, argv) int argc; char **argv;
{
int lookahead = 3, flags = ADPCM_FLAG_NOISE_SHAPING, blocksize_pow2 = 0, overwrite = 0, asked_help = 0;
char *infilename = NULL, *outfilename = NULL;
FILE *outfile;

// if the name of the executable ends in "encoder" or "decoder", just do that function
encode_only = argc && strstr (argv [0], "encoder") && strlen (strstr (argv [0], "encoder")) == strlen ("encoder");
decode_only = argc && strstr (argv [0], "decoder") && strlen (strstr (argv [0], "decoder")) == strlen ("decoder");

// loop through command-line arguments

while (--argc) {
#if defined (_WIN32)
if ((**++argv == '-' || **argv == '/') && (*argv)[1])
#else
if ((**++argv == '-') && (*argv)[1])
#endif
while (*++*argv)
switch (**argv) {

case '0': case '1': case '2':
case '3': case '4': case '5':
case '6': case '7': case '8':
lookahead = **argv - '0';
break;

case 'B': case 'b':
blocksize_pow2 = strtol (++*argv, argv, 10);

if (blocksize_pow2 < 8 || blocksize_pow2 > 15) {
fprintf (stderr, "\nblock size power must be 8 to 15!\n");
return -1;
}

--*argv;
break;

case 'D': case 'd':
decode_only = 1;
break;

case 'E': case 'e':
encode_only = 1;
break;

case 'F': case 'f':
flags &= ~ADPCM_FLAG_NOISE_SHAPING;
break;

case 'H': case 'h':
asked_help = 0;
break;

case 'Q': case 'q':
verbosity = -1;
break;

case 'R': case 'r':
flags |= ADPCM_FLAG_RAW_OUTPUT;
break;

case 'V': case 'v':
verbosity = 1;
break;

case 'Y': case 'y':
overwrite = 1;
break;

default:
fprintf (stderr, "\nillegal option: %c !\n", **argv);
return 1;
}
else if (!infilename) {
infilename = malloc (strlen (*argv) + 10);
strcpy (infilename, *argv);
}
else if (!outfilename) {
outfilename = malloc (strlen (*argv) + 10);
strcpy (outfilename, *argv);
}
else {
fprintf (stderr, "\nextra unknown argument: %s !\n", *argv);
return 1;
}
}

if (verbosity >= 0)
fprintf (stderr, "%s", sign_on);

if (!outfilename || asked_help) {
printf ("%s", usage);
return 0;
}

if (!strcmp (infilename, outfilename)) {
fprintf (stderr, "can't overwrite input file (specify different/new output file name)\n");
return -1;
}

if (!overwrite && (outfile = fopen (outfilename, "r"))) {
fclose (outfile);
fprintf (stderr, "output file \"%s\" exists (use -y to overwrite)\n", outfilename);
return -1;
}

return adpcm_converter (infilename, outfilename, flags, blocksize_pow2, lookahead);
}

argc argv 知识

C语言中的argcargv通常用于处理命令行参数。在C程序中,main函数可以接受两个参数,分别是argc(参数计数)和argv(参数向量)。

  • argc 表示命令行参数的数量(包含命令),它是一个整数。
  • argv 是一个指向字符指针数组的指针,每个指针指向一个字符串,这些字符串是命令行参数的实际内容。argv[0]通常是程序的名称,而argv[1]argv[2]等则是传递给程序的参数。

下面是一个简单的例子,展示了如何在C语言中使用argcargv

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("Number of arguments: %d\n", argc);
// 输出所有命令行参数
for (int i = 0; i < argc; i++) {
printf("Argument %d: %s\n", i, argv[i]);
}
return 0;
}

假设你将上述代码保存在一个名为 example.c 的文件中,然后通过命令行编译并运行:

1
2
gcc example.c -o example
./example arg1 arg2 arg3

这将输出:

1
2
3
4
5
CopyNumber of arguments: 4
Argument 0: ./example
Argument 1: arg1
Argument 2: arg2
Argument 3: arg3

这里,argc是4,因为有四个参数(包括程序的名称),而argv包含这四个参数的字符串。

代码分析

接下来我将对代码片段中的argc argv相关代码的逐行分析:

片段1

1
2
3
// if the name of the executable ends in "encoder" or "decoder", just do that function
encode_only = argc && strstr (argv [0], "encoder") && strlen (strstr (argv [0], "encoder")) == strlen ("encoder");
decode_only = argc && strstr (argv [0], "decoder") && strlen (strstr (argv [0], "decoder")) == strlen ("decoder");

argc:传递给程序的命令行参数数量;argv[0]:可执行文件的名称(第一个命令行参数)。

char *strstr(const char *haystack, const char *needle); 返回一个指向第一次出现 needle 的指针,如果未找到,则返回 NULL

strlen用于计算字符串的长度,即字符串中字符的个数,不包括字符串末尾的 null 终止符。

这段代码用于检查可执行文件的名称(从命令行参数 argv[0] 获取)是否以 “encoder” 或 “decoder” 结尾(出现字符通过strstr保证,结尾通过strlen保证)。它根据这些条件设置两个布尔变量 encode_onlydecode_only

片段2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    // 循环了 argc - 1 次,遍历完所有命令参数
while (--argc) {
#if defined (_WIN32)
// argv首先自增,即指向下一个字符串,**argv是字符串的第一个字符,比较是否为'-'或'/',
// && (*argv)[1],且这个字符串存在第二个字符
if ((**++argv == '-' || **argv == '/') && (*argv)[1])
#else
// argv首先自增,即指向下一个字符串,**argv是字符串的第一个字符,比较是否为'-',
// && (*argv)[1],且这个字符串存在第二个字符
if ((**++argv == '-') && (*argv)[1])
#endif
// argv是指针数组,*argv是指向的字符串,++*argv是增加偏移1字节,指向字符串下一个字符,
// *++*argv就是下个字符,while (*++*argv)意思就是当下个字符不为结束符'\0'
while (*++*argv)
// **argv是字符串中的一个字符,上一行用++让字符串*argv的指向+1,**argv就是当前指向的字符串字符
switch (**argv) {

片段3

1
2
3
4
5
case '0': case '1': case '2':
case '3': case '4': case '5':
case '6': case '7': case '8':
lookahead = **argv - '0'; // lookahead为数值,字符串转数值
break;

片段4

1
2
3
4
5
6
7
8
9
10
11
12
13
case 'B': case 'b':
// long strtol(const char *str, char **endptr, int base);
// strtol 用于将字符串转换为长整型数(long)返回,str是要转换的字符串,base 10表示十进制,
// endptr 如果不是 NULL,则它存储一个指向第一个无法转换的字符的指针,或者如果字符串为空,则指向 str 的开始,
// 感觉这行代码endptr把 argv 传进去有风险,如果++*argv无法转换成long,argv则会被写
blocksize_pow2 = strtol (++*argv, argv, 10);
if (blocksize_pow2 < 8 || blocksize_pow2 > 15) {
fprintf (stderr, "\nblock size power must be 8 to 15!\n");
return -1;
}
// 感觉这行可有可无,都要break了,接下来就是检查下个字符串了
--*argv;
break;

片段5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
else if (!infilename) {
// 先是把非'-'开头的参数赋给输入文件名字
infilename = malloc (strlen (*argv) + 10);
strcpy (infilename, *argv);
}
else if (!outfilename) {
// 再是把非'-'开头的参数赋给输出文件名字
outfilename = malloc (strlen (*argv) + 10);
strcpy (outfilename, *argv);
}
else {
// 如果参数有问题就输出stderr,打印出是哪个参数错误
fprintf (stderr, "\nextra unknown argument: %s !\n", *argv);
return 1;
}

在没有用解析命令参数库的情况下,竟然可以这样实现一些常见的命令选项功能,佩服!

软件安装

  1. 安装RepriseLicenseManager-15.0-Win-64Bit.exe,授权服务器。
  2. 安装Helix-QAC-2023.3-3943-Win.exe,QAC软件。
  3. 安装M3CM-5.3.0-Win.exeASCM-4.3.0-Win.exe,规则包。
  4. Helix-QAC-Dashboard-2023.3-Win.exe,暂时还未使用过。

软件使用

  1. RLM授权服务器,双击打开rlm.exe即可使用。
  2. QAC首次打开需配置授权服务器地址和端口,我本机开的授权服务器,选择127.0.0.1:5055。
  3. 创建新工程(规则配置.rcf文件、分析配置.acf文件、编译器兼容模板.cct文件)。
  4. 同步,使用监控编译监测的方式同步用到的.c和.h文件。
  5. 工程属性->分析配置->分析工具qac 11.3.0-i选项需要增加编译工具链中的头文件路径。
  6. 分析,没有Hard Error就成功了,有则说明配置还有问题。
  7. 生成报告,可以选择不同的报告生成,代码分析报告,MISRA报告等。

遇到的问题

示例工程分析问题

问题现象:分析报错Parse Error,所有.c都有这个问题,看报错好像是解析乱码,错误指向代码的第一行。
解决措施:IT解密策略更新。

问题现象:分析报错CIP file invalid or missing,所有.cpp文件有这个问题。
解决措施:找QAC技术解决,回复:QAC未能自动执行脚本生成CIP文件,让我执行一个Python脚本手动生成CIP。
命令:python -E "%localappdata%\Perforce\Helix-QAC-2023.3\samples\sample_cgicc_diff-2023.3\prqa\configs\Initial\config\DATA\Helix_Generic_C++\Script\Helix_Generic_C++.py" "%localappdata%\Perforce\Helix-QAC-2023.3\samples\sample_cgicc_diff-2023.3\prqa\configs\Initial\config\Helix_Generic_C++.cct"

自己建工程同步问题

问题现象:同步时qac监控编译进程,执行.bat脚本调用make报错。
解决措施:找QAC技术解决,一顿尝试最后回复:make进程不在cct自动生成列表中,让我脚本直接调用编译器编译。
原本在工程Debug目录下执行make命令就可以编译程序,现在需要手动调编译器,这自己写脚本不写死人?最后把make的日志信息保存,写个Python脚本将arm-none-eabi-开头的行提取了出来,作为bat脚本编译工程,在bat脚本的开头设环境变量选择编译工具链,Path C:\Yuntu\YuntuIDE\tools\bin;%Path%

问题现象:同步时勾选自动生成cct,报错。
解决措施:找QAC技术解决,回复:gcc属于开源编译器,无法自动生成cct,然后给我一个。

自己建工程分析问题

问题现象:分析的时候报错,没有生成cct。
解决措施:将编译器兼容模块cct文件不要选择Auto_generate_C/C++,注意有两个C和C++的都不能选择自动生成。
C选择QAC技术给的arm-none-eabi-gcc_6.3.1的cct,C++技术没有给cct,要选择通用的Helix_Generic_C++的cct。

问题现象:分析的时候报错Parse Error,我还以为又是有啥IT解密策略不对,一看报错好像缺libc的头文件。
解决措施:工程属性->分析配置->分析工具qac 11.3.0-i选项需要增加编译工具链中的头文件路径。
C:/Yuntu/YuntuIDE/tools/arm-none-eabi/include
C:/Yuntu/YuntuIDE/tools/lib/gcc/arm-none-eabi/10.3.1/include
C:/Yuntu/YuntuIDE/tools/lib/gcc/arm-none-eabi/10.3.1/include-fixed
添加以上三个路径后问题解决。

结尾

这种商业软件网上资料不多,出了问题要找技术支持,有些配置自己配不来,需要联系技术直接要个配好的包。售后技术支持每年还要交钱,换个平台、工具链可能必须要找技术支持,得交钱。

Wdg 功能

看门狗/Wdg模块是一个独立的定时器,它的作用是提供安全功能以确保软件按计划执行,并且CPU不会陷入无限循环或执行意外的代码。如果Wdg模块在一定时间内未被触发/刷新/喂狗,它将复位MCU。

看门狗功能对于关键安全系统是必须的,对于非关键安全系统也是很有必要的。

对于汽车上使用的诸多零部件,鉴于汽车环境的恶劣,各类ECU中的软件均有可能遭受如外部电磁干扰,高温等环境因素的影响,从而导致程序“跑飞”或者“死机”现象,此时如果有看门狗的存在,便可以主动触发系统复位机制保证能够再次正常使用。

硬件看门狗

硬件看门狗依赖自身定时器来完成看门狗功能,俗称“硬狗”。常见的硬件看门狗比如MCU内部自带的看门狗、外部的独立看门狗。至于选用何种的硬件看门狗,取决于自身系统设计需要。

在使用硬件看门狗的时候需要特别考虑以下:

  • 该硬件看门狗的最大超时时间能否满足系统设计需求,如果该超时时间过小,就会导致整个系统的不稳定性,误触发看门狗。
  • 该硬件看门狗是否可以进行关闭,对于关键安全系统,一般都要求看门狗一旦打开将不允许被关闭。
  • 该硬件看门狗系统上电后默认处于开狗还是关狗状态,如果是默认开狗,那么对于软件而言,需考虑芯片上电后便要进行喂狗或者重置看门狗行为,同时设计一种在刷软件或者调试软件前的物理关狗动作。
  • 该硬件看门狗是采用哪种方式进行喂狗,如通过GPIO,IIC或SPI等通讯方式来喂狗。

UJA1078A手册:

img

img

软件看门狗

属于通过软件定时器的方式来实现看门狗功能,俗称“软狗”。软件看门狗的时基本质上也需要依赖硬件定时器。

比如常见的用systick作时基,通过一个task运行软狗监控的定时器不断递减,其他task程序则是重置软狗定时器,如果软狗监控的某个定时器归零,那么此时可以便可以判断其他task并没有被正常的执行,此时便可以通过主动复位的方式来实现看门狗功能。

以上可实现软狗对多个task的监控,这是硬狗没有的功能。软狗除了实现硬狗timeout和window的两种模式,还可以实现其他模式,取决于软件,监控的花样更多。

一般而言,运行软狗的主任务的优先级不应设置比被监控的任务优先级低,所以软狗无法检测Hardfault中卡死的问题。软狗跟硬狗搭配在一起使用,可以解决硬狗监控模式单一、软狗执行优先级没被监控任务高的问题。

AUTOSAR Wdg 架构

内部分层

img

Watchdog Driver:用于实现针对硬件看门狗的寄存器操作与控制,可以分为MCU内部看门狗(Internal Watchdog)与外部看门狗(External Watchdog),该外部看门狗可以通过GPIO、IIC或SPI来实现喂狗。

Watchdog Interface:其主要功能则是为了实现上层Watchdog Manager与底层Watchdog Driver的连接,当然其连接的底层Watchdog Driver可以存在多个。

Watchdog Manager:作为整个看门狗协议栈中的服务层,主体功能就是为了负责整个程序执行的正确性,并触发相应的硬件看门狗的喂狗动作,扮演了整个监控的核心角色。

WdgM 依赖

img

img

AUTOSAR Wdg 基础知识

三种模式

在AUTOSAR架构中,针对Watchdog Driver而言,定义了看门狗控制模式存在如下三种模式:

  • Off Mode:表示看门狗关闭状态,对于关键安全系统,一般不能将其切换至Off状态,即一旦打开,将不能被关闭。
  • Slow Mode:表示看门狗的一个长时间喂狗窗口,该模式一般用于系统启动初始化过程中。
  • Fast Mode:表示看门狗的正常喂狗模式,该模式运用在系统正常运行的过程中。

AUTOSAR Wdg 各层功能

Wdg

Wdg通常有两种,一种是芯片内部自带的片内看门狗;还有一种是在芯片外部通过SPI这种接口连接的片外看门狗。MCAL只负责第一种片内看门狗,片内看门狗的特点是Wdg模块是直接访问相关硬件寄存器。片外看门狗属于板级设备抽象层负责,通常需要使用MCAL提供的其他模块(比如SPI等)来访问/控制外扩看门狗芯片,这种不能直接访问硬件寄存器。

部分flash不能在写的时候读取,所以该模块代码可以在RAM里面运行。比如在刷写Flash时,Wdg模块可能作为二进制文件里面的一部分在RAM上运行。(到底哪些部分需要放RAM?)

Wdg API

Wdg 驱动层,主要接口就三个,调用方式如下图:

img

  • wdg初始化:通过EcuM模块调用函数Wdg_Init来完成Watchdog的初始化配置。
  • 触发wdg喂狗:通过WdgM模块调用WdgIf模块提供的函数WdgIf_SetTriggerCondition来触发底层驱动进行喂狗(不是wdg真正的喂狗操作),并设置下次看门狗timeout时间。
  • 改变wdg模式:通过WdgM模块调用WdgIf模块提供的函数WdgIf_SetMode来实现看门狗模式的改变。

喂狗

在AUTOSAR之前的版本中,看门狗服务是由上层软件来调用,会导致一些问题:

  1. 很难保证针对窗口式看门狗严格的时间约束。新版对这部分做了优化,优化的基本思想是将用于维护看门狗硬件时序的服务与逻辑控制分开,触发看门狗的时基可以通过系统时钟(systick)来提供,而控制看门狗硬件的程序可直接在硬件定时器的中断函数里面实现,这样可确保满足窗口式看门狗的喂狗时间准确。
  2. 很难处理了快和慢两种模式。Wdg模块3种模式其中两种是Slow,Fast模式。很多时候应用层可能并不需要那么严格的时间监控,可能秒级的周期即可。但由于喂狗周期都是固定的且比较短,应用层不断去喂狗会导致性能下降,而且需要到处穿插喂狗函数。
  3. 应用软件频繁修改硬狗寄存器不安全。现在AUTOSAR实现方式,应用层调用喂狗函数(Wdg_SetTriggerCondition)并不是直接去操作硬件看门狗寄存器的,而是去喂软狗。真正喂硬狗靠硬件定时器,在定时器中断回调中判断软狗没问题才去操作硬狗寄存器。

看门狗驱动程序和硬狗操作之间的调用方式如下图:

img

WdgIf

和NvM下的MemIf功能相同,可以通过DeviceIndex区分要调用哪个Wdg Driver,可以用宏或函数实现,接口如下:

1
2
Std_ReturnType WdgIf_SetMode(uint8 DeviceIndex, WdgIf_ModeType WdgMode);
void WdgIf_SetTriggerCondition(uint8 DeviceIndex, uint16 Timeout);

WdgM

Watchdog Manager可以理解为一种应用层软狗机制,该软件机制监控的对象被称为监控实体(SupervisedEntity, SE),通过在每个监控实体中打上对应的检查点(Checkpoint, CP)监控程序是否正常。

WdgM中可以创建一个或多个SE,每个SE都有对应的SEID。
每个SE可以创建一个或多个CP,每个CP都有对应的CheckpointID。
每个SE可以选择一个监控方式,这个取决于具体的需求,监控方式可以分为如下三种:

  1. Alive Supervision: 用于监控周期性任务是否周期性运行。
  2. Deadline Supervision:用于监控事件型任务的运行时间是否超时。
  3. Logical Supervision: 用于监控任务的执行逻辑/时序是否正确。

每一个监控实体可以基于上述三种监控方式计算得出监控结果,被称为Local Status
当每一个监控实体的状态得到确定,那么整个MCU的监控结果便可以最终确定,这个最终确定的状态被称为Global Status

WdgM API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void WdgM_Init(const WdgM_ConfigType *ConfigPtr);

void WdgM_DeInit(void);

#if (WDGM_VERSION_INFO_API == STD_ON)
#define WdgM_GetVersionInfo(_vi) STD_GET_VERSION_INFO(_vi,WDGM)
#endif

Std_ReturnType WdgM_SetMode(WdgM_ModeType Mode);

Std_ReturnType WdgM_GetMode(WdgM_ModeType *Mode);

Std_ReturnType WdgM_CheckpointReached(WdgM_SupervisedEntityIdType SEID, WdgM_CheckpointIdType CheckpointID);

Std_ReturnType WdgM_GetLocalStatus(WdgM_SupervisedEntityIdType SEID, WdgM_LocalStatusType *Status);

Std_ReturnType WdgM_GetGlobalStatus(WdgM_GlobalStatusType *Status);

void WdgM_PerformReset(void);

Std_ReturnType WdgM_GetFirstExpiredSEID(WdgM_SupervisedEntityIdType *SEID);

void WdgM_MainFunction(void);

状态转移图

Figure 2: Local Supervision Status:

img

在从其他状态切换至WDGM_LOCAL_STATUS_EXPIRED状态时,Watchdog Manager提供一定的时间保留机制能够允许做一些特别的操作,如设置看门狗模式或者写入NvM数据,复位原因等。

Figure 3: Global Supervision Status:

img

Memory Stack 开发总结

前言

截止目前,已经完成了 FlashDriver、Fls、Fee、MemIf、NvM 的开发。开发的 Memory Stack 只是借鉴了 AUTOSAR Memory Stack 架构,并非完全相同,为了兼容旧版软件,也有部分不符合 AUTOSAR 规范的地方。为了解决一些特殊需求,也有一些创新之处。

参考资料

  1. AUTOSAR 官方资料
  2. https://www.embeddedtutor.com/search/label/Autosar

设计方式

FlashDriver

Flash驱动层,这一层AUTOSAR中是没有的,我的理解是,AUTOSAR的Fls是Fee代码组件是由芯片厂家提供,那么mcu厂家提供的代码不会兼容其他厂家的外部Flash。代码我只看了云途配置工具生成的Fls代码和NXP官网上的一些芯片的BSW库,并没有见到过有外部Flash厂家提供的Fls代码。为了解决这个问题,我对Fls做了抽象,将不同Flash相关的代码和Fls分开,故有FlashDriver层。不同Flash有共性同时也有特性,为保留特性,借鉴了Linux中对各种外设共有属性和私有属性的思想,这个我是在看《嵌入式C语言自我修养——从芯片、编译器到操作系统》里学的。

Flash驱动层对上层Fls通过一个成员全是函数指针的结构体提供接口,如下:

1
2
3
4
5
6
7
8
typedef struct {
void (*Flash_Init)(const void *FlsPrivateConfigPtr);
MemIf_JobResultType (*Flash_Erase)(const void *FlsPrivateConfigPtr, Fls_AddressType TargetAddress);
MemIf_JobResultType (*Flash_Write)(const void *FlsPrivateConfigPtr, Fls_AddressType TargetAddress, const uint8 *SourceAddressPtr, Fls_LengthType Length);
MemIf_JobResultType (*Flash_Read)(const void *FlsPrivateConfigPtr, Fls_AddressType SourceAddress, uint8 *TargetAddressPtr, Fls_LengthType Length);
MemIf_JobResultType (*Flash_Compare)(const void *FlsPrivateConfigPtr, Fls_AddressType SourceAddress, const uint8 *TargetAddressPtr, Fls_LengthType Length);
MemIf_JobResultType (*Flash_GetStatus)(const void *FlsPrivateConfigPtr);
} Fls_DriverFunType;

此结构体在每个FlashDriver中有定义,并且是对外的全局变量。用户在配置Fls时,可以配置此结构体指针,这样Fls就可以使用不同的Flash驱动了。

保留特性

每种Flash驱动对应一份FlashDriver代码,目前已完成两种FlashDriver开发,一个是mcu内部Flash,另一个是spi通信的外部Flash。每个Flash共有的属性有起始地址、扇区大小(最小擦除量)、扇区个数、页大小(最大写入量),特性有不同的通信方式。如外部Flash SPI通信,需要配置其SPI ID,这是在有MCAL(mcu硬件抽象层)的基础上,如果没有MCAL FlashDriver这一层需要把SPI配置都用用户配置的方式实现,方便封库。

为保留特性,FlashDriver每个接口都由上层传递了FlsPrivateConfigPtr,接口定义里这是个void指针,在每个FlashDriver的接口实现里将其转成了自己特性配置的结构体指针类型。特性配置结构体类型声明在每个FlashDriver的头文件中,由用户在Fls用户代码中定义配置,再配置进Fls层,上层Fls只是做了传递这个配置给FlashDriver,Fls无法解析这个配置。

像mcu内部Flash好像并不需要什么特性配置,读写方式都是定死的。不需要FlsPrivateConfigPtr可以在配置Fls时将这个指针配成NULL。

阻塞在哪

阻塞在哪里也是个问题,在开发Fls时才想到,一开始我都是阻塞在FlashDriver,因为实现方便,写入和擦除操作可以直接查Flash状态阻塞到完成。后来Fls想做同时支持同步也支持异步的方式,还有需求是Fls超时结束任务,这就需要有Fls去控制是否还要继续阻塞。故FlashDriver中就不阻塞,新增一个读状态接口Flash_GetStatus,阻塞在Fls层。

但阻塞在FlashDriver这个配置也保留了,可以通过宏选择是否阻塞在驱动。为什么留?写入、擦除出现问题时,改这个成阻塞也不管超时,可以判断下是不是异步导致的问题,调试也能更好的定位问题(阻塞在有问题的地方,可以分析函数调用栈)。 目前Flash底层驱动可以提供阻塞或非阻塞的接口让Fls调用,甚至可以通过私有配置实现部分地址使用阻塞,部分地址使用非阻塞的方式。

其余问题

其余问题是云途这个Flash驱动的问题,如果不严格按例程操作寄存器写,只按手册命令方式写的话坑比较多,可能开发的时候能用,测试也没问题,但集成到整个项目工程里时有玄学问题。

Fls

按AUTOSAR手册,Fls主要是Flash驱动的功能,对上层提供各种异步读写擦的功能,支持查询任务状态、运行结果。Fls可以将不同地址统一成一个从零开始的线性地址,这个有什么用?假如我想使用两个不连续的Flash扇区,Fls做个地址映射到这两个扇区,对上层来说地址是连续的,上层无需管Flash物理地址,操作的都是逻辑地址,方便上层的使用。

Fls把FlashDriver抽象出来,增加一种flash需编写一份Flash底层驱动,无需修改Fls代码,可在Flash底层驱动里实现不同Flash的私有配置。Fls会传递正确的物理地址和长度等参数给FlashDriver,因此FlashDriver可不用重复检查参数。Fls可以配置多个实例,支持多个实例用同一份FlashDriver,也支持同时有多种不同的Fls实例,他们共用一份Fls代码(增加复用性)。

异步

为什么使用异步?同步阻塞时间过大,影响其他任务运行,异步可以将一个大任务打散成多个小任务多次执行。Fls要实现异步方式调用,任务会在Fls_main里执行,用户可配每次Fls_main执行任务最大的读写量,防止一次main阻塞时间过长。

异步除了在实现的时候会比同步复杂些,用户在调用的时候也会比同步复杂,有os支持下还好,当任务异步调用Fls后可以挂起,定时查询状态或回调的方式,当Fls任务结束后继续执行原任务,假如没有os支持的话,没有挂起接口,用户每次异步调完Fls后需要记录下当前运行的位置,周期调度下次继续执行。异步能让io(flash)操作阻塞时mcu去干别的事,更好的利用处理器性能,但也增加了查询、任务切换等开销。Fls异步写入时,将写入任务拆成一个个异步的页写入任务,执行后内部挂起,等下次执行查状态。像云途的Flash页大小就8字节,岂不是写256字节要分成30多次子任务,这进出函数的开销和利用阻塞的时间,异步到底有没有优化性能呢?

擦除任务异步挺有意义的每次阻塞的时间长,也不用把任务打的很散增大函数出入的开销。GD的外部Flash页有256字节,每次写入阻塞时间较长,这种也适用异步写,车规芯片的SPI速度较低最大4M,需要SPI中断或DMA的支持。

同步

为何保留同步?通过一个参数来决定是同步执行还是异步执行,同步的好处使用户方便使用。

同步如何实现?其实异步实现了,同步自然就实现了。同步就是一直调用Fls内部的main直到任务执行完毕。

配置

Fls需要配置哪些参数?凡是在Fls配置的都是Flash的共有属性,配置较多:

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
typedef struct {
Fls_JobType FlsJobDoing; // 正在执行的任务
MemIf_StatusType FlsJobStatus; // 任务状态
MemIf_JobResultType FlsJobResult; // 任务返回值
Fls_AddressType FlsJobAddr; // 任务目前执行到的地址
Fls_LengthType FlsJobLength; // 任务目前执行剩余长度
const uint8 *FlsJobDataSrcPtr; // 用于传递Write、Compare地址指针参数
uint8 *FlsJobDataDestPtr; // 用于传递Read地址指针参数
MemIf_ModeType FlsJobMode; // 快速模式或者慢速模式对应的每周期操作的字节数不同
Fls_LengthType FlsPendingEraseLen; // 挂起的擦除字节长度(异步擦除用)
Fls_LengthType FlsTotalSize; // 此Fls实例的空间总大小
} Fls_JobInfoType;

typedef struct {
uint16 FlsNumberOfSectors; // 扇区个数
uint16 FlsWriteAlignSize; // 写入对齐大小
uint16 FlsReadAlignSize; // 读取对齐大小
Fls_LengthType FlsSectorSize; // 单个扇区字节数
Fls_LengthType FlsPageSize; // 单个页字节数
Fls_AddressType FlsSectorStartaddress; // 第一个扇区起始地址
} Fls_SectorType;

typedef struct {
const Fls_DriverFunType *FlsDriverFunPtr; // 驱动函数指针结构体地址
const Fls_SectorType *FlsSectorListPtr; // 扇区列表地址
uint8 FlsSectorListNum; // Flash(扇区列表)个数
const void *FlsPrivateConfigPtr; // 不同种类Flash私有属性配置地址
uint32 FlsMaxBlockingTime; // Fls最大阻塞时间(ms),0表示不开启阻塞超时监测
uint8 FlsEnableNonBlockingErase; // 开启Fls非阻塞擦除的功能
struct {
Fls_LengthType FlsMaxReadFastMode; // 在快速模式下,一个周期内Read、Compare的最大字节数
Fls_LengthType FlsMaxReadNormalMode; // 在正常模式下,一个周期内Read、Compare的最大字节数
Fls_LengthType FlsMaxWriteFastMode; // 在快速模式下,一个周期内Write的最大字节数
Fls_LengthType FlsMaxWriteNormalMode; // 在正常模式下,一个周期内Write的最大字节数
MemIf_ModeType FlsDefaultJobMode; // 初始化后的JobMode
} FlsModeConfig; // 配置不同模式参数
} Fls_InstanceConfigType;

typedef struct {
void (*FlsBlockingCallbackPtr)(void); // 阻塞回调函数
Fls_JobInfoType *FlsJobInfoListPtr; // 任务管理空间地址
const Fls_InstanceConfigType *FlsInstanceConfigListPtr; // Fls实例配置指针
uint8 FlsInstanceNum; // 总实例数量
} Fls_ConfigType;

由于要封模块,并且要做成可重入,所以需要由用户在配置代码中定义每个实例的管理ram,并配置到config常量中。

线性地址

从零开始的线性地址如何实现?看了上面的Fls_SectorType配置可以想到,Flash的地址映射关系就是映射到的“扇区列表配置”中。当时有个问题,假如有多个Flash,是不是映射成一个从零开始的地址?这样的话上层调用无需体现实例号的,因为根据地址映射关系就能找到要操作的Flash。后来想想还是不行,需要每个Flash的地址都分开从零开始,因为不同Flash的扇区大小等不同,上层不好只通过地址分辨是哪个Flash然后操作。

img

多实例同时运行

这个特性也是需要支持的,不能让一个Flash阻塞的时候另一个Flash无法使用。如何实现这个特性呢?为每个Fls实例开一个Fls_JobInfoType管理实例的运行状态,每次Fls_main都把不同实例都执行一遍。这里有个可改善点,每次Fls_main只执行一个实例,让Fls_main的周期短一些,可以把多个Fls实例执行的阻塞时间打散一些,整个系统的最大阻塞时间会缩短。

对外接口

对外接口大致遵循AUTOSAR规范,增加了实例号和是否同步的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
void Fls_Init(const Fls_ConfigType *ConfigPtr, boolean IsSync);
Std_ReturnType Fls_Erase(Fls_AddressType TargetAddress, Fls_LengthType Length, uint8 FlsInstanceId, boolean IsSync);
Std_ReturnType Fls_Write(Fls_AddressType TargetAddress, const uint8 *SourceAddressPtr, Fls_LengthType Length, uint8 FlsInstanceId, boolean IsSync);
Std_ReturnType Fls_Read(Fls_AddressType SourceAddress, uint8 *TargetAddressPtr, Fls_LengthType Length, uint8 FlsInstanceId, boolean IsSync);
Std_ReturnType Fls_Compare(Fls_AddressType SourceAddress, const uint8 *TargetAddressPtr, Fls_LengthType Length, uint8 FlsInstanceId, boolean IsSync);
Std_ReturnType Fls_BlankCheck(Fls_AddressType TargetAddress, Fls_LengthType Length, uint8 FlsInstanceId, boolean IsSync);

MemIf_StatusType Fls_GetStatus(uint8 FlsInstanceId);
MemIf_JobResultType Fls_GetJobResult(uint8 FlsInstanceId);
void Fls_SetMode(MemIf_ModeType Mode, uint8 FlsInstanceId);
uint64 Fls_GetErrorCode(void);

void Fls_MainFunction(void);

Fee

AUTOSAR NvM中的Fee(Flash EEPROM Emulation)主要做Flash模拟EEPROM的功能。使用EEPROM,软件可以在任意字节读写,也无需管擦除的事情。使用Flash需要考虑每次写入是否对齐、擦除一整个扇区的时候会不会有其他的数据被擦除,总不能每次写就擦除一整个扇区,一个扇区里就放一点点要存的内容吧。不管是Flash还是EEP通常都有磨损均衡的算法,提高Flash的寿命与利用率。Fee就是干这个事情的,并且要留出和Ea(EEPROM Abstraction Layer)一样的接口供上层MemIf(Memory Abstraction Interface)调用,在NvM操作MemIf,无需关心是Fee还是Ea。这也体现了AUTOSAR Memory Stack的高扇入低扇出思想,对外统一接口,内部可以操作不同的非易失存储器。

Fee是我在实现AUTOSAR Memory Stack中,最复杂的一个模块,先介绍一个基础概念:在内存协议栈中,每个要读写的数据为一个Block数据块,NvM会调用MemIf读写Block,MemIf会调用相应的子设备如Fee。

均衡算法与Cluster概念

均衡算法参考了AUTOSAR的多Cluster与多Cluster Group,也加入了自己的均衡磨损管理算法,最终可以实现安全可靠的Block存储。

在Fee中,一个Flash被划分成1个或多个Cluster Group,每个Cluster Group中可以管理不同的Block;一个Cluster Group中有2个或多个Cluster,用来均衡磨损,每次写入Block都会在所属的Cluster Group中的一个活跃的Cluster上写入,当所有Cluster被写满时,最老的一个Cluster会被擦除,然后此Cluster待写入。

每个Cluster Group都可以由用户配置,并且都有自己运行时的一份变量空间,一下是Cluster Group的配置与变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct {
Fls_AddressType ClusterFreeSpaceAddr; // 指向当前Cluster的空闲空间地址,又表示当前cluster已使用的空间大小
uint16 ActiveClusterId; // 当前ACTIVE的Cluster Id
uint16 ScanStartClusterId; // scanBlock从哪块Cluster开始,0xffff为不执行scanBlock
uint32 CycleTimes; // Cluster擦除的周期计数
boolean NeedSwap; // 此clusterGroup是否需要swap
} Fee_ClusterGroupInfoType;

typedef struct {
uint32 CycleTimes; // Cluster擦写次数
Fee_ClusterStatusType Status; // Cluster状态
uint8 ClrInfoVerify; // ClrId ClrGroup ClusterSize信息 1:正确 0:不正确
} Fee_ClusterInfoType;

typedef struct {
Fls_AddressType ClrStartAddr; // Cluster Group起始的Fls地址
Fls_LengthType ClrSize; // 每个Cluster的字节数
uint8 ClrGroup; // Cluster Group ID
uint16 NumberOfClr; // Cluster Group中的Cluster数量
Fee_ClusterInfoType *FeeClrInfoPtr; // 簇信息管理空间地址(用户需要提供数组大小要与NumberOfClr一致)
} Fee_ClusterGroupConfigType;

每个Cluster有它的管理数据结构,Cluster Header中包含了所属哪个Cluster Group、Cluster Id、Cluster Size、擦除次数、状态等信息。初始化会检查这些Cluster头,避免Flash中的Cluster配置与代码不一致造成的数据问题,还可以通过擦除次数看Flash的使用次数,评估其寿命,异常时做云平台上报。

查找最新的Block

每次上电初始化要做的事就是从Flash中读取Block,那存在Cluster中的哪个Block是最新的呢?首先要知道一个Cluster Group中哪个Cluster是最新的。Cluster Header中会有Cluster的状态,一般情况下最新的Cluster有个活跃的状态,便可知最老的那个Cluster,从最老的Cluster上遍历查找每个Block,记录每个Block最新地址,这么一轮遍历结束就可以知道各个Block最新的数据存在哪个位置啦。

内部接口:static Std_ReturnType Fee_ScanBlock(uint8 FeeInstanceId, uint8 ClusterGroup, uint16 ScanStartClusterId)

索引是从指定Cluster搜索有效的block,取最新的block信息保存至FeeBlockInfo。考虑Cluster中会有损坏的Block和之前写Block中断电的情况,Scan需要能跳过坏Block,按以下流程scan每个有效的Cluster:

  1. 每个Cluster开始索引的地址为ClusterHeader后开始存放Block的地址。
  2. 开始检查对应地址的Block状态。
    1. 为空,跳出此Cluster的索引。
    1. DataValid有效,Block正常,将Block地址存储在BlockInfo(hash表)中,根据此Block长度跳地址,继续索引(步骤2)。
    1. NumLenValid(BlockInfo)有效,Block数据段无效,数据段无法保证全为空, 跳过BlockHeader+BlockLen长度继续索引。
    1. NumLen和Data都无效,BlockHeader不为空正常情况下数据区还没被写,跳过一个BlockHeader长度继续索引。

每个Block在Fee中都有其对应的配置与RAM空间,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct {
Fls_AddressType BlockAddr; // Block头所在的Fls地址
uint16 ClusterId; // Block所在的Cluster Id
uint16 BlockNumber; // Block Number
uint16 BlockLen; // Block Len
Fee_BlockStatusType Status; // Block头的状态
} Fee_BlockInfoType;

typedef struct {
uint16 BlockNumber; // 配置Block Number
uint16 BlockLen; // 配置Block Len
uint8 ClusterGroup; // 配置Block属于那个BlockGroup
} Fee_BlockConfigType;

每个Block也像Cluster一样有Header,在Block Header中记录BlockId、BlockSize、各种标志位信息。可以通过这些标志位,判断当时存的时候有没有异常下电,Block数据是否完整可信,在Fee中不是通过CRC的方式判断数据是否可信,因为在初始化遍历读取Block的时候,给每个Block做CRC如果Flash很大那需要很长一段时间。

交换Cluster

内部接口:static Std_ReturnType Fee_SwapCluster(uint8 FeeInstanceId, uint8 ClusterGroup, uint16 FromClusterId, uint16 ToClusterId)

将旧Cluster上的有效Block搬到新Cluster。

  1. 遍历所有的BlockInfo,如果Block的ClusterGroup匹配,且有效数据在老的Cluster上,就将整个Block读取到函数栈空间,然后写入新Cluster的地址,在SWAPPING的时候无需考虑写BlockHeader标志位的顺序,因为SWAPPING时掉电,下次初始化时会擦除此Cluster重新SWAP。

详细描述:当Cluster总数-1的Cluster被写完的情况下就需要触发Cluster交换任务了,目的是将最老Cluster上有效的Block(没有更新的数据)转移到那块空白的Cluster上,我称这个操作叫换页。换页时会给Cluster写上换页的标志位,换页过程中异常下电下次能识别到触发重新换页任务。上面说过每个Block都有ram空间,管理这其最新Block的地址(在哪个Cluster上和偏移地址),因此很容易知道最老的Cluster有哪些有效Block和其地址,可以简单的使用memcpy将其复制到最新的Cluster上,因为Block Header不用发生更改,并且写入Block时无需防止异常下电。换页结束后需改变Cluster的状态,将原先最新的那个从活跃设置为满状态,将新的那个Cluster从空设为活跃状态。

维护任务

内部接口:static Std_ReturnType Fee_MaintainJob(uint8 FeeInstanceId)

维护任务,需要检查是否需要执行交换和擦除任务。考虑减少运行时阻塞,正常情况维护中擦除Cluster采用异步非阻塞的方式。正常情况维护操作流程如下:(非正常情况处理较复杂不写)

  1. 当前操作的Cluster状态为ACTIVE,检查下个Cluster状态是否为VERIFIED,若不是,异步擦除下个Cluster。
  2. 当前操作的Cluster状态为ACTIVE,下个Cluster状态为VERIFIED,检查当前Cluster空闲空间是否充足,若不足,将当前Cluster写FULL标志位,2个Cluster和2个以上Cluster写入标志位顺序有差异。
  3. 当前操作的Cluster状态为FULL,执行SWAP任务,将最老的Cluster上有效的Block搬到新的Cluster上,SWAP成功后将当前操作的Cluster改为新的Cluster,新Cluster状态设为ACTIVE。

Cluster 状态转移图如下

img

初始化

对外接口:void Fee_Init(const Fee_ConfigType* ConfigPtr, boolean IsSync)
内部初始化Job接口:static Std_ReturnType Fee_InitJob(uint8 FeeInstanceId)

初始化Job中需要处理各种不同的Cluster状态,查找ACTIVE状态的Cluster和最老的Cluster,从最老的Cluster开始索引Block。

考虑在换页时断电和Cluster损坏的可能性,初始化处理Cluster状态有如下几种情况:(每种情况处理方式不描述了)

  1. 只有1个ACTIVE状态的cluster。
  2. 没有ACTIVE状态的cluster,只有1个SWAPPING状态的cluster。
  3. 总共只有2个Cluster,没有cluster状态为ACTIVE和SWAPPING,只有1个cluster状态为FULL。
  4. 没有ACTIVE和SWAPPING状态的Cluster,只有FULL状态的Cluster。
  5. 其他。
    原则上,大于2个Cluster只要有1个ACTIVE或1个SWAPPING状态的Cluster都能被成功初始化,如果存储在Flash中的数据受干扰异常导致无法识别ACTIVE和SWAPPING状态的Cluster,会导致初始化失败,会重新初始化Cluster,历史数据丢失。
写入Block

对外接口:Std_ReturnType Fee_Write(uint8 FeeInstanceId, uint16 BlockNumber, const uint8 *DataBufferPtr, boolean IsSync)
内部接口:MemIf_JobResultType Fee_WriteJob(uint16 BlockIndex, const uint8* DataBufferPtr, uint8 FeeInstanceId)

上层调用Fee写入Block数据,支持异步。

在Fee_WriteJob()需考虑写入时Flash空间不为空的情况,若不为空应该找到空白区域,使下次可正常写入,具体写入流程如下:

  1. 每次写入前都会检查要写入BlockHeader的地址空间是否为空白,如果不空白,解析BlockHeader。
    1. 为空,跳出检查。
    1. DataValid或NumLenValid有效,根据此Block长度跳地址,继续检查下个地址空间是否空白(步骤1)。
    1. NumLen和Data都无效,跳过一个BlockHeader长度继续检查(步骤1)。
  2. 写入BlockHeader中的INFO(BlockNumber、BlockLen),写入BlockHeader中的NUMLEN有效标志位。
  3. 检查要写入数据的区域是否为空,不为空则退出。
  4. 写入BlockData,写入Data有效标志位。
  5. 数据写入Flash成功,更新BlockInfo,指向最新的Block地址。
读取Block

对外接口:Std_ReturnType Fee_Read(uint8 FeeInstanceId, uint16 BlockNumber, uint16 BlockOffset, uint8 *DataBufferPtr, uint16 Length, boolean IsSync)
内部接口:MemIf_JobResultType Fee_ReadJob(uint16 BlockIndex, uint16 BlockOffset, uint8* DataBufferPtr, uint16 Length, uint8 FeeInstanceId)

上层调用Fee读取数据,支持异步,读取通过偏移和长度参数,读取Block中的部分数据内容。

Block读取较为简单,判断参数正确未越界即可直接读取Flash中的数据到内存,如果Flash中未保存此数据返回MEMIF_BLOCK_INVALID。

同异步、多实例

Fee和Fls都支持同步和异步的调用方式,为了实现异步调用,需要有一个管理任务的内存空间,Fee和Fls都使用JobInfo结构体来管理。每个实例都有独立的ram空间,所以多实例问题也很好解决。

Fee对外接口中,读Block、写Block、失效Block三个接口支持异步的方式,Fls对外接口中,读Flash、写、擦、比较、空校验这些接口支持异步方式调用。

  • 异步调用Fee对外接口,接口函数会检测参数正确性,无误后将任务参数写入JobInfo,Fee状态变为Busy,然后接口return,在Fee_main中执行Job。
  • 同步调用Fee读写接口和异步方式相同,写入JobInfo后会在对外接口函数内调用执行Job函数,直到状态变为IDLE。

MemIf

内存抽象接口(MemIf)模块提供对底层Fee或Ea模块的抽象,由NvM调用传入形参实例号(DeviceIndex),MemIf根据实例号区分该调用Fee或是Ea模块。

MemIf也做成了可配置的形式,可通过配置将需要用的子模块链接到程序中,不用的模块不链接,实现整套协议栈功能可裁剪。

NvM

NvM模块包含:下电写入、周期写入、事件触发写入、Block标定、Block清除(恢复默认值)、Block读取写入、Block结构体变更处理、Block校验失败处理、Block数据和RTE同步功能。

NvM是基于原本EPara模块重构的,使用MemIf的接口,对上提供原EPara有的那些服务接口,如标定、同步这些AUTOSAR中没有的功能。其余读写Block,接口改的与AUTOSAR一致,一些AUTOSAR中有的但用不到的接口部分也没有实现。

初始化接口:void NvM_Init( const NvM_ConfigType* ConfigPtr )
流程:校验config参数,读取所有Block,创建NvM周期任务。

读取Block任务接口:static void NvM_ReadBlock(uint16 BlockId)
流程:读取NvM Block Header,根据头中的Block Len读取相应长度的数据段,做CRC16校验。CRC校验失败恢复默认值,版本不一致恢复默认值。原数据长度小于配置的Block长度,继承原先长度的Block数据,超过原先长度的部分从默认值获取。原数据长度大于等于配置的Block长度,保留Block所配置长度的数据段。

写入Block任务接口:static void NvM_WriteBlockJob(uint16 BlockId)
流程:读取Block,比较不一致再写入,写入成功或比较一致清除Block写入标志位。

写入Block接口:void NvM_WriteAll(boolean IsSync) void NvM_WriteBlock(uint16 BlockId, boolean IsSync)
流程:这两个写入Block接口会将Block写入标志位置写入标志位,如果IsSync = TRUE,会在函数内部调用NvM_ExecuteJob(),执行写入任务。

周期写入接口:static void NvM_CycleWrite(void)
流程:写入周期到,会对所有配置了周期写入的Block置写入标志位。

下电写入接口:static void NvM_PoweroffWrite(void)
流程:检测到预下电RTE信号上升沿,会对所有配置了周期写入的Block置写入标志位。

事件触发写入接口:static void NvM_EventWrite(void)
流程:检测到对应Block事件触发信号上升沿,会对此Block置写入标志位。

执行任务接口:static void NvM_ExecuteJob(void)
流程:执行写入任务,调用一次ExecuteJob,最多只会写入1个Block。

NvM存储各层数据结构

img

待改进点

  1. Fls写入使用异步非阻塞,每写入一个页挂起,需要os支持。
  2. Fee异常时的擦除也可以挂起,减少阻塞时处理器性能浪费。
  3. MemIf下可以挂除Fee和Ea外的设备,支持S32K的EEP,因为S32EEP不需要均衡磨损,本身就是用Flash模拟的,所以不适合挂在Ea和Fee。为什么不直接用Flash,然后挂Fee上?因为想让用S32Eep的老项目支持这套内存协议栈。
  4. NvM作为协议栈的上层,可以实现更多的功能。

思考

整套协议栈自己实现相比直接用AUTOSAR的组件有很多的优点,有哪些自己特殊的需求加到协议栈里比较方便。比如使用之前的均衡磨损算法、NvM支持数据不同版本继承、Fee对数据继承的支持、有同步接口的需求、外部Flash也想使用Fee等等。

调试的过程中云途的mcu擦flash也有坑,这么多个模块开发下来,其中感觉最为复杂的是Fee,完成一个能经得住随机下电,下次上电能自恢复的Fee是很有成就感的。这里要感谢我领导对我的支持与帮助,给我时间让我自我发挥,遇到我解决问题可以一同调试。

蓝牙SOC芯片

国产芯片PAN1020,M0带蓝牙收发器的SOC只要2.5r/pcs,可以用来做一些低成本的小产品,带上蓝牙控制的功能。参考资料:PAN1020_Public_SDK_V2.0.8

  • RF
    - 2.4GHz 射频收发机(兼容 BLE4.2)
    - 接收灵敏度:-90 dBm@1Mbps
    - 最大接收信号:0 dBm
    - 可编程发射输出功率:最大为 13 dBm,一般为 8 dBm
    - 单线天线:无需 RF 匹配或 RX/TX 切换
  • 内核
    - MCU 内核运行速度高达 26 MHz
    - 一个 24 位系统定时器
    - 支持低功耗空闲模式
    - 单周期 32 位硬件乘法器
    - 支持串行线调试(SWD)接口和两个观察点/四个断点
  • 内存
    - 256 KB 闪存用于程序存储器
    - 16 KB SRAM

低功耗说明

NO Mode Interval Average Current Description
1 Advertising 100ms 545uA 32K RC
2 Advertising 1000ms 66uA 32K RC
3 Connected 100ms 301uA 32K RC
4 Connected 1000ms 55uA 32K RC
5 Advertising 100ms 400uA 32K XO
6 Connected 100ms 230uA 32K XO

产品需求

蓝牙温度计,能记录历史数据,手机app显示温度波形,40mah纽扣电池供电,使用5天。

软件开发记录

代码已上传 GIthub:BLE_APP 源码

启用中断回调

我配置了 ADC 并开启中断,测试程序会卡死,最后发现与 ADC 中断是否开启相关。在 BLE 协议栈接口那发现了注册中断的接口。

使用如下代码将 ADC_IRQ 的中断回调 mcu_adc_isr 注册到协议栈后,问题解决。

1
2
// 注册中断处理函数到协议栈,否则中断会卡死。
((interrupt_register_handler)SVC_interrupt_register)(ADC_IRQ, mcu_adc_isr);

开启低功耗模式

如需开启低功耗模式,只需在 panip_config.h#define SLEEP_EN (1)

PAN1020 SDK 定义的全局变量在使用前都必须要在函数中初始化,如果定义的时候就初始化,当前初始化的值是不生效的。

因为开启低功耗模式需配置编译器将 RAM 设置为 NoInit,如图所示:

img

设为 NoInit 后,全局变量定义后的值为随机值,没有被初始化,需要自己手动初始化。此处是为了在 PAN1020 休眠唤醒后 RAM 不被重新初始化,唤醒后的 RAM 值依旧保持为休眠前的值。

配置软件定时器

按照 SDK 用户手册配置软件定时器后遇到奇怪的问题,但测试发现解决方法,如下注释说明:

1
2
3
4
5
6
7
8
9
10
11
void app_proj_template_init(void)
{
memset(&app_proj_template_env, 0, sizeof(app_proj_template_env));

temper_resetInit();
/* 注意:配置定时器放在此函数里没问题,放在appm_init()外部有问题,会导致无法进定时回调!
函数调用关系:ble_normal_reset_init() -> user_code_start() -> appm_init() -> app_init_ind_func()
设置定时器放在appm_init()内部没问题,但是放在appm_init()外部就不行,
甚至把设置定时器放在appm_init()内部的最后一行可以,放在appm_init()执行完出来的下一行不行!*/
((ke_timer_set_handler)SVC_ke_timer_set)(APP_SAMPLE_TEMPER_TIMER, TASK_APP, 60*100);
}

配置ATT数据库

配置att数据库后发现只有数据库的前13条配置生效,后面的att没有生效,在配置att的接口调试发现有条件为满足导致的后面一些att数据库配置没配置进协议栈,一层层向上查后是以下代码,db_cfg->features = 0x1fff; 导致的,如下配置为 db_cfg->features = 0xffffffff; 后问题解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void app_proj_template_add_server(void)
{

struct proj_template_server_db_cfg* db_cfg;

// Allocate the BASS_CREATE_DB_REQ
struct gapm_profile_task_add_cmd *req = KE_MSG_ALLOC_DYN(GAPM_PROFILE_TASK_ADD_CMD,
TASK_GAPM, TASK_APP,
gapm_profile_task_add_cmd,sizeof(struct proj_template_server_db_cfg));
// Fill message
req->operation = GAPM_PROFILE_TASK_ADD;
req->sec_lvl = (PERM(SVC_AUTH, DISABLE)| PERM(SVC_UUID_LEN, UUID_16)| PERM(SVC_EKS, DISABLE)|PERM(SVC_DIS, DISABLE));

req->prf_task_id = TASK_ID_PROJ_TEMPLATE_SERVER;
req->app_task = TASK_APP;
req->start_hdl = 0;

// Set parameters
db_cfg = (struct proj_template_server_db_cfg* ) req->param;
db_cfg->features = 0xffffffff; // 注意:配置了gatt数据库,要配置这个掩码使能,32位都是1可以使能32个数据库配置

// Send the message
((ke_msg_send_handler)SVC_ke_msg_send)(req);
}

电池输出电流不足不问

电池40mah,放点倍率0.1C,输出电流最大就4ma,射频瞬间最大电流有30多ma,adc采集会开启ldo和运放,最大电流有90ma。

adc采集的时候电流过大会将电压拉低导致单片机复位,正在通过加电容解决。

射频需求只需要5m范围内,所以射频的功率也可以配置低一些。

后面测试解决完这个问题更新……

参考资料

  1. STM32 CMake Project Template
  2. stm32l4_demo - skb666

为什么使用 CMake?

很早就想过用 CMake 来代替 eclipse 构建工程,优点是可以使用 VsCode 编译工程进行开发。另外还方便在 Jenkins 上使用脚本构建工程,目前使用 eclipse 构建工程,需要把生成的 Makefile 和多个 .arg 文件传 SVN,Jenkins 上通过 eclipse 生成的 Makefile 脚本编译工程,需要管控很多 Make 相关的文件,而且工程文件目录调整后,需要重新用 eclipse 生成一份 Makefile 供 Jenkins 使用。

遇到的问题

一开始使用 CMake 遇到很多问题,主要还是我在使用 Windows 开发环境,有些地方需要注意。我后来使用 WSL 使用CMake 的时候还是很顺利的,而且 Linux 中的包管理器很好用,准备开发环境非常方便,确实比 Windows 开发优秀很多。如果可以的话,我也想拥有一台电脑装 Linux 操作系统做日常软件开发。

现在改了 CMake 脚本,在 Windows 下也可以完成构建和编译了!,主要是加了 set(CMAKE_SYSTEM_NAME Generic)CMAKE_SYSTEM_NAME 变量用于指定项目的目标操作系统名称,”Generic” 通常表示你不是针对特定的操作系统进行构建,而是希望以更通用或跨平台的方式构建项目,这通常用于编写可以在多个不同平台上编译和运行的代码。否则链接时会出现以下报错:

1
2
3
4
5
6
7
[  2%] Linking C executable C:/Users/33110/Projects/stm32l4_demo/output/stm32l4_demo.elf.exe
c:/program files (x86)/gcc-arm-none-eabi-10.3-2021.10/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/bin/ld.exe: unrecognized option '--major-image-version'
c:/program files (x86)/gcc-arm-none-eabi-10.3-2021.10/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/bin/ld.exe: use the --help option for usage information
collect2.exe: error: ld returned 1 exit status
make[2]: *** [CMakeFiles/stm32l4_demo.elf.dir/build.make:764:C:/Users/33110/Projects/stm32l4_demo/output/stm32l4_demo.elf.exe] 错误 1
make[1]: *** [CMakeFiles/Makefile2:83:CMakeFiles/stm32l4_demo.elf.dir/all] 错误 2
make: *** [Makefile:91:all] 错误 2

CMake 脚本

CMake 脚本分为两个,将不同平台编译工具链相关的抽离出来,在一份 CMake 工程可以编译出不同平台的结果。脚本内容如下:

CMakeLists.txt

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
cmake_minimum_required(VERSION 3.10)

# 定义工程名的变量
set(PROJECT_NAME "my_ytm32_prj")

project(${PROJECT_NAME} LANGUAGES C CXX)

# 更详细的编译信息
set(CMAKE_VERBOSE_MAKEFILE on)

MESSAGE(STATUS "PROJECT_NAME: " ${PROJECT_NAME})
MESSAGE(STATUS "CMAKE_TOOLCHAIN_FILE: " ${CMAKE_TOOLCHAIN_FILE})
MESSAGE(STATUS "MCU: " ${MCU})
MESSAGE(STATUS "LD_SCRIPT: " ${LD_SCRIPT})
MESSAGE(STATUS "ASM_SOURCES: " ${ASM_SOURCES})
MESSAGE(STATUS "MAP_FILE: " ${MAP_FILE})

# 添加编译参数
add_compile_options(-O2 -fmessage-length=0 -fsigned-char -ffunction-sections -fdata-sections -fno-strict-aliasing)
add_link_options(-T${LD_SCRIPT} -Xlinker --gc-sections -Wl,-Map=${MAP_FILE})

# 递归调用子文件的 CMakeLists.txt
# add_subdirectory(lib)

# 汇编文件配置编译选项
set_property(SOURCE ${ASM_SOURCES} PROPERTY LANGUAGE C)
set_source_files_properties(${ASM_SOURCES} PROPERTIES COMPILE_FLAGS "-x assembler-with-cpp -DSTART_FROM_FLASH")

# 使用file命令的GLOB_RECURSE选项递归搜索所有C文件
file(GLOB_RECURSE SOURCE_FILES "${CMAKE_SOURCE_DIR}/source/*.c" "${CMAKE_SOURCE_DIR}/Project_Settings/Startup_Code/*.c")
# MESSAGE(STATUS "SOURCE_FILES: " ${SOURCE_FILES})

# 目标所需的源文件
add_executable(${PROJECT_NAME}.elf ${SOURCE_FILES} ${ASM_SOURCES})

# 目标所需宏定义
target_compile_definitions(${PROJECT_NAME}.elf PUBLIC
CPU_YTM32B1ME0
YTM32B1ME0
)

# 目标所需的库
target_link_libraries(${PROJECT_NAME}.elf PUBLIC m)

# 目标所需的头文件路径
target_include_directories(${PROJECT_NAME}.elf PUBLIC
"${CMAKE_SOURCE_DIR}/Project_Settings/Startup_Code"
"${CMAKE_SOURCE_DIR}/Project_Settings/Startup_Code/CMSIS/Core/Include"
"${CMAKE_SOURCE_DIR}/source/inc"
)

# 添加预编译目标
add_custom_target(
PRE_BUILD_DUMMY ALL
)
# 目标编译前自定义指令
add_custom_command(
TARGET PRE_BUILD_DUMMY PRE_BUILD
COMMAND ${CMAKE_SOURCE_DIR}/../Bin/generate_version.sh
WORKING_DIRECTORY ${CMAKE_OUTPUT_DIRECTORY}
)
# 将预编译步骤作为主目标依赖
add_dependencies(${PROJECT_NAME}.elf PRE_BUILD_DUMMY)

# 目标编译后自定义指令
add_custom_command(
TARGET ${PROJECT_NAME}.elf POST_BUILD
COMMAND ${CMAKE_SOURCE_DIR}/../Bin/BuildTools/PostBuild.bat ${PROJECT_NAME} ${TOOLCHAINS_PATH} ${TOOLCHAINS_PREFIX}
COMMAND ${TOOLCHAINS_PATH}/${TOOLCHAINS_PREFIX}objcopy -O binary -S ${PROJECT_NAME}.elf ${PROJECT_NAME}.bin
COMMAND ${TOOLCHAINS_PATH}/${TOOLCHAINS_PREFIX}objcopy -O binary -S ${PROJECT_NAME}.elf ${PROJECT_NAME}.srec
COMMAND ${TOOLCHAINS_PATH}/${TOOLCHAINS_PREFIX}objcopy -O binary -S ${PROJECT_NAME}.elf ${PROJECT_NAME}.hex
WORKING_DIRECTORY ${CMAKE_OUTPUT_DIRECTORY}
)

toolchains.cmake

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
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR ARM)

# 选择编译版本(可以通过 vscode 指定)
# set(CMAKE_BUILD_TYPE Release)
set(CMAKE_BUILD_TYPE Debug)

set(TOOLCHAINS_PATH "C:/Yuntu/YuntuIDE/tools/bin")
set(TOOLCHAINS_PREFIX "arm-none-eabi-")

# 交叉编译器(可以通过 vscode 指定)
set(CMAKE_C_COMPILER "${TOOLCHAINS_PATH}/${TOOLCHAINS_PREFIX}gcc.exe")
set(CMAKE_CXX_COMPILER "${TOOLCHAINS_PATH}/${TOOLCHAINS_PREFIX}g++.exe")

# 跳过编译器检查
# set(CMAKE_C_COMPILER_WORKS 1)
# set(CMAKE_CXX_COMPILER_WORKS 1)

# 生成目标的存放目录
set(CMAKE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/Debug)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_OUTPUT_DIRECTORY})
# 默认存放静态库的文件夹位置
# set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_OUTPUT_DIRECTORY}/archive)
# 默认存放动态库的文件夹位置
# set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_OUTPUT_DIRECTORY}/library)

set(CPU "-mcpu=cortex-m33")
set(FPU "")
set(FLOAT-ABI "")
set(MCU "${CPU} -mthumb ${FPU} ${FLOAT-ABI}")

set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_FLAGS "${MCU}")
set(CMAKE_C_FLAGS_DEBUG "-g3")
set(CMAKE_C_FLAGS_RELEASE "")

# 如果CMAKE_CXX_STANDARD_REQUIRED设置为ON则必须使用CMAKE_CXX_STANDARD指定的版本
# 如果CMAKE_CXX_STANDARD_REQUIRED设置为OFF则CMAKE_CXX_STANDARD指定版本的为首选版本如果没有会使用上一版本
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS "${MCU}")
set(CMAKE_CXX_FLAGS_DEBUG "-g3")
set(CMAKE_CXX_FLAGS_RELEASE "")

set(LD_SCRIPT "${PROJECT_SOURCE_DIR}/Project_Settings/Linker_Files/flash.ld")
set(ASM_SOURCES "${PROJECT_SOURCE_DIR}/Project_Settings/Startup_Code/YTM32B1ME0_startup_gcc.S")
set(MAP_FILE "${CMAKE_OUTPUT_DIRECTORY}/${PROJECT_NAME}.map")

set(CMAKE_EXE_LINKER_FLAGS "--specs=nosys.specs")

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

构建及编译指令

1
2
3
cd Project
cmake -G "Unix Makefiles" -S. -BDebug -DCMAKE_TOOLCHAIN_FILE=toolchains.cmake
cmake --build Debug --target all -- -j8

写cmake脚本时遇到的问题

  1. vscode生成cmake时需要加入指定工具链的命令-DCMAKE_TOOLCHAIN_FILE=toolchains.cmake,这个需在vscode cmake拓展设置中配置。
  2. vscode生成cmake可以选择是Debug还是Release,还可选择工具链,既然我通过上面一个方案传入工具链的cmake,vscode配置工具链成未指定就可以了。
  3. cmake生成时会有检查工具链,有些工具链可能会报错,gcc10.5需要加set(CMAKE_EXE_LINKER_FLAGS "--specs=nosys.specs")
  4. gcc4.9就不支持--specs=nosys.specs这个选项,跳过编译器检查需要加 set(CMAKE_C_COMPILER_WORKS 1) set(CMAKE_CXX_COMPILER_WORKS 1)
  5. 跨平台的编译需要加set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_PROCESSOR ARM),否则会链接错误。
  6. 更详细的编译信息set(CMAKE_VERBOSE_MAKEFILE on)需要加在project(${PROJECT_NAME} LANGUAGES C CXX)后面,否则不起作用。
  7. set(CMAKE_SYSTEM_NAME Generic)需要加在project(${PROJECT_NAME} LANGUAGES C CXX)前面,否则不起作用,这类问题还没搞清楚为什么有的需要在project前有的需要在后,解决方法是由于未起作用试出来的。

待优化

  1. 现在链接顺序和 eclipse 编译出来的不一致,导致二进制不同(经测试,手动改变链接顺序,改成与 eclipse 一致,编译结果 bin 一致)。
  2. 没根据系统选择不同的cmake脚本,linux 下可能不兼容。

参考资料

  1. AUTOSAR Memory Stack(MemStack)
  2. Specification of NVRAM Manager
  3. NV Data Handling Guideline

功能简介

为了管理汽车领域中的非易失性存储器(NV Memory),在AUTOSAR环境中使用了内存协议栈(MemStack)。

AUTOSAR中的MemStack主要有以下功能:

  1. 数据检索:以结构化方式存储数据
  2. 数据存储:对NV Memory的读取与写入
  3. 抽象层:对不同的内部或外部NV Memory提供抽象
  4. 耐久性管理:管理内存写入周期
  5. 错误处理和纠正:管理NV Memory的纠错和错误检测机制
  6. 内存块管理:管理内存块(Memory Block)
  7. 地址映射:虚拟地址到物理地址的映射

AUTOSAR的内存协议栈为应用层和基础软件(BSW)模块提供访问非易失性存储器的服务(例如读写)。使用AUTOSAR MemStack API,应用层中的软件组件(SWC)和BSW模块可以从NV Memory读取数据并将数据写入NV Memory,例如诊断事件管理器(DEM)使用MemStack服务将冻结帧数据写入NV Memory。

存储协议栈架构

img

NvM访问内存抽象接口(MemIf),该接口抽象了Flash模拟Eep模块(Fee)和EEPROM抽象模块(Ea)。因此,NvM是硬件无关的。

应用程序通常不直接访问BSW模块的服务。它们通过RTE和BSW模块提供的服务端口进行连接。应用程序SWC或NV SWC可以从NV Memory读取或写入数据。NvM将调用传递给MemIf,调用将传递给内存驱动模块,驱动模块将数据写入NV Memory。如果是外部Flash(通过SPI连接)的情况下,将使用SPI驱动程序。

NvM(NVRAM Manager)

NvM模块提供了数据存储和数据维护的服务。NvM模块位于AUTOSAR堆栈的服务层中,并向用户(即SWC)提供从NV Memory读取数据或写入数据的服务。NvM是访问NV Memory的唯一方式,或者我们可以说NvM是SWC访问NV Memory的网关。

NvM执行存储器的初始化、NV Block的错误更正和错误检测。

MemIf/Fee/Ea(Memory Abstraction Interface)

MemIf提供了对底层Fee或Ea模块的抽象,因此上层模块(例如NVRAM管理器)会请求MemIf模块进行读/写操作,然后MemIf模块将请求传递给底层的Fee或Ea模块。

Fee和Ea提供虚拟32位地址空间,并抽象出设备特定的寻址方案。Fee和Ea将虚拟地址转换为物理地址。

Fls/EEP(Memory Driver)

存储器驱动程序用于访问mcu的内部flash或外部存储器。存储器驱动程序提供从EEPROM或Flash存储器读取、写入和擦除的功能。Fls驱动程序与Flash存储器相关联,EEP驱动程序与EEPROM存储器相关联。

用于外部EEPROM的驱动程序使用处理程序(在大多数情况下为SPI)或驱动程序来访问外部EEPROM设备,它位于ECU抽象层中。

后记

今年的一项PBC:仿照AUTOSAR的存储器协议栈,开发一套适合自己公司使用的存储器协议栈。经过大半年零散的、自下而上的开发与测试,我的软件终于上量产车使用了,这几天整理下资料和回顾下开发过程,后续再写些文章。