0%

起因

用Github部署静态站访问国内速度慢,主要表现有2个,一是首次请求页面打开慢,二是打开后图片加载慢。问题一原因是域名解析需要访问多个海外的DNS服务器,且Github静态站服务器也在海外,首次请求国内或本地没有缓存。问题二也是访问Github静态站服务器速度受限,图片和文本并发访问,文本比图片数据量小先加载完成显示。

本想将博客全部移到Gitee上,尝试部署又遇到三个问题,一是不能使用自己的域名解析,二是部署Gitee静态站服务它竟说我有文章不合规,三是每次上传Gitee后不会自动更新静态页面,每次要重新发布审核。

本人使用的解决办法是将图片上传到gitee,github静态博客上的图片都使用gitee的链接,Gitee/blog仓库img分支上传了本博客用到的图片。

部署方式

参考:Gitee Pages

在Gitee中创建仓库,开然后将博客用到的图片上传到Gitee中,开通仓库的Pages服务更新分支。审核成功后就可以通过Gitee提供的域名,加上图片在仓库中的路径访问了。例如我的静态页面网址是:https://xxx.gitee.io/,要访问仓库210430-at32文件夹中的210430-at32-1.jpg,访问网址为:https://xxx.gitee.io/210430-at32/210430-at32-1.jpg

使用域名转发

加入以后我的图床地址变了,但我又不想重新修改每个文章里的图片网址怎么办?

使用域名转发可以解决此问题,我使用的是易名注册的域名和易名免费的域名解析服务,开启URL隐性转发,转发值为你的静态页面网址,这样就可以通过自己的域名访问了,以后想换个图床的话只需要转发地址更改下就可以了。使用易名域名解析转发到我的静态页面会审核不通过,这是因为静态页面没有内容,只需要加个index.html骗骗审核就行了,审核结束就可以删除。

遇到一个问题,使用域名转发通过http访问可以,通过https访问不行,暂且文章里的图片网址用http访问吧。

前言

这篇文章是对之前文章【LTE Cat.1模块和阿里云物联网平台使用】的一个补充。之前只介绍了阿里云物联网平台如何创建产品、添加设备、添加物模型概述,缺少对消息解析、物模型展示的使用介绍,导致我这次用阿里云物联网平台时花了将近半天的时间在做之前做过又忘记怎么做的事情。本文就来介绍下消息解析物模型展示的功能。

另外阿里云免费的物联网平台公共实例的资源包将于2023年2月20日下线,我看企业版的最便宜的也要700元/月,我这种添加一个设备调试用的不能白嫖了,到时候需要的话只能包一台服务器自己搭个MQTT划算点了,或者看看其他云服务商那能不能白嫖😂。

添加物模型

设备管理->产品->对应产品名称->功能定义->编辑草稿中添加物模型数据,功能类型有属性、服务、事件,我目前只使用到了属性类型,编辑完成后发布上线即可,下面是我这次调试模块用到的物模型功能定义:

img

消息解析

设备管理->产品->对应产品名称->消息解析->编辑草稿中编写消息解析的脚本代码,有js、Python、php三种语言可供选择,我选择的是Python。消息解析有自定义Topic消息解析物模型消息解析两种,创建产品时数据格式选择透传/自定义,消息解析里才有设备上报数据和设备接收数据,数据格式选ICA 标准数据格式(Alink JSON),消息解析里只有自定义Topic消息解析。通过看模拟输入中模拟类型有哪些,可以知道是否支持某种消息类型的数据解析。

自定义Topic消息解析

设备通过携带解析标记?_sn=default的自定义Topic上报自定义格式消息时,物联网平台收到消息数据后,需调用消息解析脚本将自定义格式数据转换为JSON结构体,再流转给后续业务系统。例如,设备发送到Topic /${productKey}/${deviceName}/user/update的消息需要解析为JSON格式,在开发设备端时,就需配置该Topic为:/${productKey}/${deviceName}/user/update?_sn=default

在Python脚本中,自定义Topic消息解析的接口函数名为transform_payload(topic, rawData),可以根据不同的topic选择不同的解析方式。

物模型消息解析

数据格式为ICA标准数据格式,设备按照物联网平台定义的标准数据格式生成消息上报,标准Alink JSON数据格式说明,请参见设备属性、事件、服务

数据格式为透传/自定义,设备通信时,需要物联网平台调用您提交的消息解析脚本,将上行物模型消息解析为物联网平台定义的标准格式(Alink JSON),将下行物模型消息据解析为设备的自定义数据格式。

在Python脚本中,设备自定义数据格式转Alink JSON格式数据的函数(上行通信)为raw_data_to_protocol,Alink JSON格式数据转为设备自定义数据格式的函数(下行通信)为protocol_to_raw_data,要注意的是raw_data_to_protocol函数需要将rawData输入转为标准的Alink JSON,参考标准Alink JSON数据格式说明。下面是我这次用到的脚本解析代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ALINK_PROP_REPORT_METHOD = 'thing.event.property.post'
def raw_data_to_protocol(rawData):
uint8Array = []
for byteValue in rawData:
uint8Array.append(byteValue & 0xff)

jsonMap = {}
params = {}
params['status'] = uint8Array[0]
params['error'] = uint8Array[1]
params['validIDNum'] = uint8Array[2]
params['errorSlave'] = uint8Array[3]
params['idallocTimes'] = uint8Array[4]
params['costTime'] = (uint8Array[6]|(uint8Array[7]<<8)) # 非单字节变量注意大小端
params['successCnt'] = (uint8Array[8]|(uint8Array[9]<<8)|(uint8Array[10]<<16)|(uint8Array[11]<<24))
params['errorCnt'] = (uint8Array[12]|(uint8Array[13]<<8)|(uint8Array[14]<<16)|(uint8Array[15]<<24))
jsonMap['params'] = params # 物模型中的属性添加到params中,再加到jsonMap
jsonMap['method'] = ALINK_PROP_REPORT_METHOD # 标准的Alink JSON必须要加method

return jsonMap

物模型显示效果

这次应用是有软件模块过年放假期间需要测试,我用4G Cat.1模块传到阿里云物联网平台记录数据,最终物模型显示效果如下图:

img

printf函数

函数原型:int printf(const char *format, ...)
调用格式为:printf("<格式化字符串>", <参量表>);
功能:发送格式化输出到标准输出 stdout

变长参数实现思路

C语言支持变长参数函数(Variable Argument Functions),即参数的个数可以是不定个,在函数定义的时候用...表示。采用这种形式定义的变长参数函数,至少需要一个普通的形参,且...需要放在最后一个参数,比如printf函数中的*format后面的...是函数原型的一部分。

变长参数的实现得益于C语言默认的cdecl调用惯例,其参数是从右向左压入栈的,第一个参数位于栈顶。这样printf函数实现的时候,就无需关心调用他的函数会传递几个参数过来,而只要关心自己用到几个,将全部参数压入栈,函数处理时从栈中取即可。

自己实现一个变长参数的函数

C已经有现成可用的一些东西来帮我们实现变长参数,它主要通过stdarg.h头文件定义的一个变量类型(va_list)和三个宏(va_start、va_arg、va_end)来实现。

实现一个可变长参数的sum函数,第一个参数num传递变长参数中有参数的数量,紧接着后面会传递num个整型变量,具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int sum(int num, ...)
{
int i, val = 0;
va_list ap; //定义一个具有va_list型的变量,这个变量是指向参数的指针
va_start(ap, num); //始化变量刚定义的va_list变量,使其指向第一个可变参数的地址,地址自动增加
for(i = 0; i < num; i++)
{
val += va_arg(ap, int); //va_arg返回va_list中的参数,并增加指针偏移
}
va_end(ap); //结束可变参数列表
return val;
}
void main()
{
printf("16+38+53=%d\n", sum(3, 16, 38, 53));
}

变长参数实现原理

上面的sum函数也可以不使用va_list等宏,通过其他方法实现。
当我们调用:int n = sum(3, 16, 38, 53);
参数在栈上会形成如下布局:
img

在函数内部,函数可以使用变量num来访问数字3,但无法使用任何名称访问其他的几个不定参数。但此时由于栈上其他的几个参数实际恰好依序排列在参数num的高地址方向,因此可以很简单地通过num的地址计算出其他参数的地址,sum函数的另一种实现如下:

1
2
3
4
5
6
7
8
int sum(int num, ...)
{
int* p = &num + 1;
int ret = 0;
while(num--)
ret += *p++;
return ret;
}

printf的不定参数比sum要复杂得多,因为printf的参数不仅数量不定,而且类型也不定。所以printf需要在格式字符串中注明参数的类型,例如用%d表明是一个整数。printf里的格式字符串如果将类型描述错误,因为不同参数的大小不同,不仅可能导致这个参数的输出错误,还有可能导致其后的一系列参数错误。[摘自《程序员的自我修养——链接、封装、库》P338]

printf("%lf\t%d\t%c\n", 1, 666, 'a'); 在这行函数里,printf的第一个输出参数是一个int(4 字节),而我们告诉printf它是一个double(8字节),因此printf的输出会错误,由于printf在读取double的时候实际造成了越界,因此后面几个参数的输出也会失败。该程序的实际输出为:0.000000 97(根据实际编译器和环境可能不同)

va_list等宏如何实现

va_list 实际是一个指针,用来指向各个不定参数。由于类型不明,因此这个 va_list 以 void* 或 char* 为最佳选择。
va_start 将 va_list 定义的指针指向函数的最后一个参数后面的位置,这个位置就是第一个不定参数。
va_arg 获取当前不定参数的值,并根据当前不定参数的大小将指针移向下一个参数。
va_end 将指针清 0。
按照以上思路,va_list等宏的一个最简单的实现就可以得到了,如下所示:

1
2
3
4
#define va_list char*
#define va_start(ap, arg) (ap=(va_list)&arg+sizeof(arg))
#define va_arg(ap, t) (*(t*)((ap+=sizeof(t))-sizeof(t)))
#define va_end(ap) (ap=(va_list)0)

注意:实际代码中还套了很多宏,不同编译器,不同架构都有可能使用不同的代码实现,但具体实现思想一致,有些x64条件编译时va_list会是一个结构体,里面会记录可变参数开始地址、结束地址、参数数量等信息。

起因

在学习FPGA时使用了DDR3,最近学ARM处理器也用到了DDR3的外设,在FPGA是使用 MIG(Memory Interface Generators)IP核驱动DDR3,ARM处理器是通过配置MMDC(Multi Mode DDR Controller)模块驱动DDR3,这编博客将会介绍使用DDR需配置的几个关键参数。

SRAM操作流程、时序图可以浏览我之前的文章,FPGA之SDRAM学习

关键参数

传输速率

传输速率的单位是MT/s(Mega Transfer Per Second),即每秒传输的百万次数,常见DDR3有800MT/s、1066MT/s、1333MT/s、1600MT/s等,这是首先需要考虑的,该参数决定了DDR的最高数据传输速率。

tRCD 参数

tRCD 全称是 RAS-to-CAS Delay,也就是行寻址到列寻址之间的延迟。 DDR 的寻址流程是先指定 BANK 地址,然后在指定行地址,最后指定列地址确定最终要寻址的单元。 BANK 地址和行地址是同时发出的,这个命令叫做行激活(Row Active)。行激活以后就发送列地址和具体的操作命令(读还是写),这两个是同时发出的,因此一般也用读/写命令表示列寻址。在行有效(行激活)到读/写命令发出的这段时间间隔叫做 tRCD。

CL 参数

当列地址发出以后就会触发数据传输,但是从数据从存储单元到内存芯片 IO 接口上还需要一段时间,这段时间就是非常著名的 CL(CAS Latency),也就是列地址选通潜伏期

AL 参数

在 DDR 的发展中,提出了一个前置 CAS 的概念,目的是为了解决 DDR 中的指令冲突,它允许 CAS 信号紧随着 RAS 发送,相当于将 DDR 中的 CAS 前置了。但是读/写操作并没有因此提前,依旧要保证足够的延迟/潜伏期,为此引入了 AL(Additive Latency),单位也是时钟周期数。 AL+CL 组成了 RL(Read Latency),从 DDR2 开始还引入了写潜伏期 WL(Write Latency),WL 表示写命令发出以后到第一笔数据写入的潜伏期。

tRAS 参数

RAS active time,也指Active to Precharge Delay,行有效至行预充电时间。是指从收到一个请求后到初始化RAS(行地址选通脉冲)真正开始接受数据的间隔时间,tRAS 是 ACTIVE 命令到 PRECHARGE 命令之间的最小时间,tRAS=tRCD+tWR。

其他参数

tRP 参数

在发出预充电命令之后,要经过一段时间才能允许发送RAS行有效命令打开新的工作行,这个间隔被称为tRP(RAS Precharge time,预充电有效时间)。

tRC 参数

tRC(Row Cycle Time),表示“SDRAM行周期时间”,它是包括行单元预充电到激活在内的整个过程所需要的最小的时钟周期数,是两个 ACTIVE 命令或者 ACTIVE 命令到 REFRESH 命令之间的周期。tRC=tRAS+tRP。如果tRC的时间过长,会因在完成整个时钟周期后激活新的地址而等待无谓的延时,而降低性能。然而如果该值设置过小,在被激活的行单元被充分充电之前,新的周期就可以被初始化,也会导致数据丢失和损坏。

tWR 参数

由于数据信号由控制端发出,输入时芯片无需做任何调校,只需直接传到数据输入寄存器中,然后再由写入驱动器进行对存储电容的充电操作,因此数据可以与CAS同时发送,也就是说写入延迟为0。不过,数据并不是即时地写入存储电容,因为选通三极管(就如读取时一样)与电容的充电必须要有一段时间,所以数据的真正写入需要一定的周期。为了保证数据的可靠写入,都会留出足够的写入/校正时间(tWR,Write Recovery Time),这个操作也被称作写回(Write Back)。tWR至少占用一个时钟周期或再多一点(时钟频率越高,tWR占用周期越多)。

起因

这几天在做ID自分配协议栈,使用的是J1939协议,汽车中还有其他的EOL协议、快充协议也都是使用的J1939协议。

SAE-J1939与CAN2.0的关系

CAN2.0是一种总线规范,是数据链路层的技术。J1939是SAE(美国汽车协会)基于CAN总线定义的的规范,主要用于解决不同发动机厂商、不同ECU厂商之间的兼容性问题。J1939定义了一系列的PGN和SPN,这些PGN包含了发动机、变速器、车轴等汽车上各部件的信息;对参数的表示方法(状态和值)又定义了SLOT(Scaling—比例、Limit—界限、Offset—偏移、Transfer—传送)。ECU厂商开发设备时都应该遵循这个规范。ECU模块的功能不同,厂商不同,在J1939的基础上,又表现出其多样性:支持或者不支持某些PGN、SPN和SLOT;新增了某些J1939未定义的PGN和SPN。

SAE-J1939消息帧格式

CAN2.0规范包括CAN2.0A(标准帧格式),CAN2.0B(扩展帧格式),二者使用不同的帧格式位码。J1939是在CAN2.0B的基础上进一步封装,对仲裁场部分的29位ID的重新定义。SAE-J1939中只为扩展帧格式定义了标准化的通信,因此,SAE-1939设备必须使用扩展帧格式

SAE-J1939数据帧结构

SAE-J1939将每个协议数据单元(PDU)融合进一个CAN2.0B数据帧中,其结构如下:

img

参数群编号(PGN)对于制定基于SAE-J1939的CAN协议来说十分重要,很多ECU厂商规定在接受CAN报文时识别的就是PGN而不是整个报文的ID。参数群编号是由24位组成的(其实是18位),主要包括下面几个部分:保留位(R,1bit,默认为:0),数据页位(DP,1bit,多数情况下为:0),PDU格式(PF,8bit)和特定PDU(PS,8bit,目标地址是否群扩展)。当PF值为:0~239之前时PGN的低字节将被设置为:0;当PF值为240~254之时,PGN的低字节为PS的值。PGN结构如下:

img

程序设计

点灯流程

  1. 使能指定 GPIO 的时钟
  2. 设置 IO 的复用功能
  3. 配置 GPIO 输出功能、上拉、速度等等
  4. 设置 GPIO 输出高电平或低电平

点灯汇编代码

代码中的地址参考《i.MX 6ULL Applications Processor Reference Manual》

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
.global _start	@ global symbol

_start:
ldr r1, =0xffffffff
mov r2, #6
ldr r0, =0x020c4080 @ CCM_CCGR6
ldr r2, =0x020c4068 @ CCM_CCGR0

CCGR_loop_init:
str r1, [r0]
sub r0, #4
cmp r2, r0
ble CCGR_loop_init

ldr r0, =0x020e0068 @ IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03
ldr r1, =0x5
str r1, [r0]

ldr r0, =0x020e02f4 @ IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03
ldr r1, =0x10b0
str r1, [r0]

ldr r0, =0x0209c004 @ GPIO1_GDIR
ldr r1, =0x8
str r1, [r0]

loop:
ldr r0, =0x0209c000 @ GPIO1_DR
ldr r1, =0x0
str r1, [r0]

ldr r0, =0x1f78a400 @ 528M
ldr r1, =0xfbc5200 @ 264M
delay1:
sub r0, #100
cmp r0, r1
bge delay1

ldr r0, =0x0209c000 @ GPIO1_DR
ldr r1, =0x8
str r1, [r0]

ldr r0, =0x1f78a400 @ 528M
ldr r1, =0xfbc5200 @ 264M
delay2:
sub r0, #100
cmp r0, r1
bge delay2

b loop

程序编译

编译

arm-linux-gnueabihf-gcc -g -c led.s -o led.o

上述命令就是将 led.s 编译为 led.o,其中“-g”选项是产生调试信息,GDB 能够使用这些调试信息进行代码调试。“-c”选项是编译源文件,但是不链接。“-o”选项是指定编译产生的文件名字。

链接

arm-linux-gnueabihf-ld -Ttext 0X87800000 led.o -o led.elf

上述命令中-Ttext 就是指定链接地址,“-o”选项指定链接生成的 elf 文件名,这里命名为 led.elf。

格式转换

arm-linux-gnueabihf-objcopy -O binary -S -g led.elf led.bin

烧录要用到bin文件,上述命令中,“-O”选项指定以什么格式输出,后面的“binary”表示以二进制格式输出,选项“-S”表示不要复制源文件中的重定位信息和符号信息,“-g”表示不复制源文件中的调试信息。

反汇编

arm-linux-gnueabihf-objdump -D led.elf > led.dis

有时候需要查看其汇编代码来调试代码,因此就需要进行反汇编,一般可以将 elf 文件反汇编,上述代码中的“-D”选项表示反汇编所有的段。

Makefile脚本

1
2
3
4
5
6
7
led.bin:led.s
arm-linux-gnueabihf-gcc -g -c led.s -o led.o
arm-linux-gnueabihf-ld -Ttext 0X87800000 led.o -o led.elf
arm-linux-gnueabihf-objcopy -O binary -S -g led.elf led.bin
arm-linux-gnueabihf-objdump -D led.elf > led.dis
clean:
rm -rf *.o led.bin led.elf led.dis

注意:每一个命令行必须以 注意:每一个命令行必须以[Tab]字符开始,不能是空格开始,[Tab] 字符告诉 make 此行是一个命令行,make 按照命令完成相应的动作。这也是书写按照命令完成相应的动作,这也是书写 Makefile 中容易产生,而且比较隐蔽的错误。报错信息:Makefile:2: *** 遗漏分隔符 (null)。 停止。

反汇编

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
Disassembly of section .text:

87800000 <_start>:
87800000: e3e01000 mvn r1, #0
87800004: e3a02006 mov r2, #6
87800008: e59f0078 ldr r0, [pc, #120] ; 87800088 <delay2+0x10>
8780000c: e59f2078 ldr r2, [pc, #120] ; 8780008c <delay2+0x14>

87800010 <CCGR_loop_init>:
87800010: e5801000 str r1, [r0]
87800014: e2400004 sub r0, r0, #4
87800018: e1520000 cmp r2, r0
8780001c: dafffffb ble 87800010 <CCGR_loop_init>
87800020: e59f0068 ldr r0, [pc, #104] ; 87800090 <delay2+0x18>
87800024: e3a01005 mov r1, #5
87800028: e5801000 str r1, [r0]
8780002c: e59f0060 ldr r0, [pc, #96] ; 87800094 <delay2+0x1c>
87800030: e59f1060 ldr r1, [pc, #96] ; 87800098 <delay2+0x20>
87800034: e5801000 str r1, [r0]
87800038: e59f005c ldr r0, [pc, #92] ; 8780009c <delay2+0x24>
8780003c: e3a01008 mov r1, #8
87800040: e5801000 str r1, [r0]

87800044 <loop>:
87800044: e59f0054 ldr r0, [pc, #84] ; 878000a0 <delay2+0x28>
87800048: e3a01000 mov r1, #0
8780004c: e5801000 str r1, [r0]
87800050: e59f004c ldr r0, [pc, #76] ; 878000a4 <delay2+0x2c>
87800054: e59f104c ldr r1, [pc, #76] ; 878000a8 <delay2+0x30>

87800058 <delay1>:
87800058: e2400064 sub r0, r0, #100 ; 0x64
8780005c: e1500001 cmp r0, r1
87800060: aafffffc bge 87800058 <delay1>
87800064: e59f0034 ldr r0, [pc, #52] ; 878000a0 <delay2+0x28>
87800068: e3a01008 mov r1, #8
8780006c: e5801000 str r1, [r0]
87800070: e59f002c ldr r0, [pc, #44] ; 878000a4 <delay2+0x2c>
87800074: e59f102c ldr r1, [pc, #44] ; 878000a8 <delay2+0x30>

87800078 <delay2>:
87800078: e2400064 sub r0, r0, #100 ; 0x64
8780007c: e1500001 cmp r0, r1
87800080: aafffffc bge 87800078 <delay2>
87800084: eaffffee b 87800044 <loop>
87800088: 020c4080 andeq r4, ip, #128 ; 0x80
8780008c: 020c4068 andeq r4, ip, #104 ; 0x68
87800090: 020e0068 andeq r0, lr, #104 ; 0x68
87800094: 020e02f4 andeq r0, lr, #244, 4 ; 0x4000000f
87800098: 000010b0 strheq r1, [r0], -r0
8780009c: 0209c004 andeq ip, r9, #4
878000a0: 0209c000 andeq ip, r9, #0
878000a4: 1f78a400 svcne 0x0078a400
878000a8: 0fbc5200 svceq 0x00bc5200

和我写的汇编代码都是一一对应的,只是把直接数放在了代码段的最后,ldr通过pc+offset来取。

从反汇编来看还把ldr一些短的直接数改成了mov指令。

起因

云途MCU内存有ECC(Error Correcting Code)功能,需要在startup.s中初始化所有内存(既赋值0),所以软复位后从startup.s中reset_handle运行,会重新初始化内存,原本内存的值会被清0,无法使用内存OTA升级程序,需要用到Flash来保存OTA信息。

这篇文章来讲下汇编启动程序做了什么,单片机启动过程,ld链接脚本中定义的变量在汇编程序中的引用,不同编译器汇编程序的区别。

参考文档

  1. 正点原子《I.MX6U 嵌入式 x Linux 驱动开发指南 V1.6 6》——第七章 ARM 汇编基础
  2. ARM开发人员网站,可以直接搜索指令
  3. ARM资源图书馆,可以下载白皮书、ARM编程手册
  4. Documentation for binutils,binutils工具链(ld, as…)的官方文档

启动程序和启动过程

常用指令

2020032320502673.png

startup_stm32f40_41xxx.s 代码分析

1
2
3
4
5
6
7
8
9
10
Stack_Size      EQU     0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp

Heap_Size EQU 0x00000200
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit

第1行:EQU 是表示宏定义的伪指令,类似于 C 语言中的#define。伪指令的意思是指这个“指令”并不会生成二进制程序代码,也不会引起变量空间分配。0x00000400 表示栈大小,字节为单位。0x00000400 =1024字节=1KB。

第2行:开辟一段数据空间可读可写,段名 STACK,按照8字节对齐。ARER 伪指令表示下面将开始定义一个代码段或者数据段。此处是定义数据段。ARER 后面的关键字表示这个段的属性。

  • STACK :表示这个段的名字,可以任意命名。
  • NOINIT:表示此数据段不需要填入初始数据。
  • READWRITE:表示此段可读可写。
  • ALIGN=3 :表示首地址按照 2 的 3 次方对齐,也就是按照 8 字节对齐(地址对 8 求余数等于0)。

第3行:SPACE 这行指令告诉汇编器给STACK段分配 0x00000400 字节的连续内存空间。

第4行:__initial_sp 紧接着SPACE语句放置,表示了栈顶地址。__initial_sp 只是一个标号,标号主要用于表示一片内存空间的某个位置,等价于 C 语言中的“地址”概念。地址仅仅表示存储空间的一个位置,从 C 语言的角度来看,变量的地址,数组的地址或是函数的入口地址在本质上并无区别。

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
                PRESERVE8   ; 指定当前文件保持堆栈8字节对齐
THUMB ; 表示后面的指令是THUMB指令集


; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors ; EXPORT申明标号为可被外部引用
EXPORT __Vectors_End
EXPORT __Vectors_Size

__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
DCD MemManage_Handler ; MPU Fault Handler
DCD BusFault_Handler ; Bus Fault Handler
DCD UsageFault_Handler ; Usage Fault Handler
...... 省略
DCD OTG_HS_IRQHandler ; USB OTG HS
DCD DCMI_IRQHandler ; DCMI
DCD CRYP_IRQHandler ; CRYP crypto
DCD HASH_RNG_IRQHandler ; Hash and Rng
DCD FPU_IRQHandler ; FPU
__Vectors_End
__Vectors_Size EQU __Vectors_End - __Vectors

上面这块代码初始化了中断向量表,第一个是SP指针初始化地址,后面是中断向量表,包含异常处理和外设中断,DCD会定义个4Bytes空间存储中断要跳转的地址。这块RESET数据段放在Flash开始,程序从Flash首地址开始运行,先初始化SP和PC(PC就是Reset_Handler),再跳转去Reset_Handler执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
                AREA    |.text|, CODE, READONLY
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK] ; 弱定义
IMPORT SystemInit
IMPORT __main
LDR R0, =SystemInit
BLX R0 ; 跳转至SystemInit()函数初始化时钟
LDR R0, =__main ; 跳转至__main()初始化堆栈, __main()由MDK自动生成
BX R0
ENDP

; Dummy Exception Handlers (infinite loops which can be modified)

NMI_Handler PROC
EXPORT NMI_Handler [WEAK]
B .
ENDP
...... 省略

上面这块定义了中断服务函数,都是弱定义,用户可以在别的文件中重定义。除了Reset_Handler有实现,其他都为死循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
;*******************************************************************************
; User Stack and Heap initialization
;*******************************************************************************
IF :DEF:__MICROLIB
EXPORT __initial_sp
EXPORT __heap_base
EXPORT __heap_limit

ELSE
IMPORT __use_two_region_memory
EXPORT __user_initial_stackheap

__user_initial_stackheap
LDR R0, = Heap_Mem
LDR R1, =(Stack_Mem + Stack_Size)
LDR R2, = (Heap_Mem + Heap_Size)
LDR R3, = Stack_Mem
BX LR
ALIGN
ENDIF

启动代码的最后一部分,简单的汇编语言实现 IF ELSE语句。如果定义了__MICROLIB,那么程序是不会执行ELSE分支的代码。MDK中MicroLIB的作用,参考:KeilMDK配置项中Use MicroLIB

起因

之前UART用的比较多,232, 485, 422通信方式用的比较少,最近储能的项目用到了485通信,在之前卡片机的项目也用到485通信,今天来归纳下232, 485, 422这三种通信的区别。

参考资料

  1. 本文搬运自 转载:串口通信232/485/422 详细解析! ,自己复习使用

UART

通用异步收发传输器(Universal Asynchronous Receiver/Transmitter)。

通信协议

UART作为异步串口通信协议的一种,工作原理是将传输数据的每个字符一位接一位地传输,其中各位的意义如下:

起始位:先发出一个逻辑”0”的信号,表示传输字符的开始。

数据位:紧接着起始位之后。数据位的个数可以是4、5、6、7、8等,构成一个字符。通常采用ASCII码。从最低位开始传送,靠时钟定位。

奇偶校验位:数据位加上这一位后,使得”1”的位数应为偶数(偶校验)或奇数(奇校验),以此来校验数据传送的正确性。

停止位:它是一个字符数据的结束标志。可以是1位、1.5位、2位的高电平。 由于数据是在传输线上定时的,并且每一个设备有其自己的时钟,很可能在通信中两台设备间出现了小小的不同步。因此停止位不仅仅是表示传输的结束,并且提供计算机校正时钟同步的机会适用于停止位的位数越多,不同时钟同步的容忍程度越大,但是数据传输率同时也越慢

空闲位:处于逻辑”1”状态,表示当前线路上没有数据传送。

图片

波特率与比特率

比特率在数字信道中,比特率是数字信号的传输速率,它用单位时间内传输的二进制代码的有效位(bit)数来表示,其单位为每秒比特数bit/s(bps)、每秒千比特数(Kbps)或每秒兆比特数(Mbps)来表示(此处K和M分别为1000和1000000,而不是涉及计算机存储器容量时的1024和1048576)。

波特率指数据信号对载波的调制速率,它用单位时间内载波调制状态改变次数来表示,其单位为波特(Baud)。波特率与比特率的关系为:比特率=波特率X单个调制状态对应的二进制位数。

如何区分两者?显然,两相调制(单个调制状态对应1个二进制位)的比特率等于波特率;四相调制(单个调制状态对应2个二进制位)的比特率为波特率的两倍;八相调制(单个调制状态对应3个二进制位)的比特率为波特率的三倍;依次类推。

232

232 通信主要是由RX,TX,GND三根线组成。RX与TX,TX接RX,GND接GND。因为发送和接收分别是由不同的线处理的,也就是能同时发送数据和接收数据,这就是所谓的全双工。

图片

在这里扩展一下,串口通信还有一个功能叫做全功能串口通信,也叫标准串口。因为在两个设备间进行数据传输,有些设备处理速度比较快,有些数据比较慢。为了保证数据能正常传输,在RX,TX的基础上,还增加了几个控制引脚,本来好端端就R,T,G,三根线,凑着就凑齐了9个引脚,召唤出了DB9这个东西。

图片

这要怪就怪当时使用电脑的时候,还没有互联网这个概念,但是又想在两台电脑间进行通信。所以才有这样一个东西。在后来的设备,很多控制器,人机界面,PLC等使用串口通信中,基本上就不使用标准串口,而是就直接使用RX,TX,GND三根线来通信了。但是这里为什么要提到这个呢。因为只是很多设备这样用,也就是还存在少数设备还保留了标准串口的功能。这就是为什么会遇到明明电脑通信是好的,换成触摸屏通信就不行了。因为很多触摸屏只使用了RX,TX,GND通信,遇到一些还保留标准串口功能的就比较讨厌了。

485

485是为了解决232通信距离的问题。原理什么之类的就不多讲了。反正232通信距离就是不长。485主要是以一种差分信号进行传输,只需要两根线,+,-两根线,或者也叫A,B两根线。A,B两根线的差分电平信号就是作为数据信号传输。

那么问题来了,那是不是就没有RX和TX的概念了。是的,发送和接收就不能分开了。发送和接收都是靠这两根的来传输,也就是每次只能作发送或者只能作接收,这就是半双工的概念了,这在效率上就比232弱很多了。就像对讲机一样,经常是某个人讲完之后,都要说一个over,确保当前说完了,等待对方回复。

图片

485就是这样牺牲了232全双工的效率来达到自己传输距离远的代价。那有没有即保留了232的全双工,又可以像485这样提高传输距离呢,于是,422出来了。

422

422呢,有些标注为485-4。而485就标注为485-2。有什么区别呢。就是为了好记呢。485-2就是2根线。485-4就是4根线。

图片

422就是把232的RX分成两根线,RX+,RX-,把TX分成TX+,TX-。这样就可以同时发送和同时接收了,还可以像485这样,有较远的传输距离。可是这样一种很有优势的通信方式,为什么用的不多呢。我个人的答案和理解就是:线太多了。特别是像我这样懒得接线的人,超过3根线就头晕的。搞个通信还需要接这么多线,什么TX,RX,正啊负啊。交换来交换去。

因为在很多设备通信中,基本上是属于一问一答式的,因此,232的全双工通信优势其实也并没有发挥出来。就像现在打电话,虽然两个人可以同时说话,但是两个人同时说话,叽叽歪歪的,谁知道说什么呀。特别是一个主站与多个从站通信的时候,485的接线就就方便多了,反正大家就两根线,把+都接一块,把-都接一块。如果是422作一主多从,接线上还要理半天呢,而且通信异常了也不好解决。

参数解析

argparse —- 命令行选项、参数和子命令解析器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import argparse

# 创建一个解析器
parser = argparse.ArgumentParser(description='use for Encrypting/Decrypting.')
# 添加参数
parser.add_argument('-d', action="store_true", help='Decryption command')
parser.add_argument('-e', action="store_true", help='Encryption command')
parser.add_argument('-r', action="store_true", help='Rename command')
parser.add_argument('-p', action="store", required=True, help='file path or folder path')

if __name__ == '__main__':
args = vars(parser.parse_args())
print(args)

指令 -p 为必须,通过 -p 传入文件或文件夹路径参数,指令 -d -e -r 告知脚本要执行什么任务。

Python vars() 函数 返回对象object的属性和属性值的字典对象。

1
2
PS C:\decipherer> python main.py -p .\test1.c
{'d': False, 'e': False, 'r': False, 'p': '.\\test1.c'}

解析路径

Python os.path() 模块 —- 主要用于获取文件的属性

1
2
3
4
5
6
main_file_path = os.path.dirname(os.path.realpath(__file__))
file_name = os.path.basename(file_path)
file_content = fp.read()
file_name_md5 = hashlib.md5(file_name.encode(encoding='UTF-8')).hexdigest()
file_relative_path = os.path.realpath(file_path).replace(main_file_path, '') # 相对地址
file_relative_dir = os.path.dirname(file_path).replace(main_file_path, '') # 相对地址不带文件名

Python3 os.walk() 方法 —- 用于遍历文件夹

1
2
3
4
def rename_dir(dir_path):
for root, dirs, files in os.walk(dir_path, topdown=False):
for name in files:
rename_file(os.path.join(root, name))

shutil.rmtree() —- 删除一个完整的目录树

1
2
if os.path.exists('./output_rename'):
shutil.rmtree('./output_rename') # 删除之前的目录

makedirs() —- 递归目录创建函数

1
2
if os.path.exists('./output_rename' + file_relative_dir) == 0:
os.makedirs('./output_rename' + file_relative_dir) # 如果目录不存在 创建新目录

MD5加解密

hashlib —- 安全哈希与消息摘要

字符串编解码

方法 描述
string.decode(encoding=’UTF-8’, errors=’strict’) 以 encoding 指定的编码格式解码 string,如果出错默认报一个 ValueError 的 异 常 , 除非 errors 指 定 的 是 ‘ignore’ 或 者’replace’
string.encode(encoding=’UTF-8’, errors=’strict’) 以 encoding 指定的编码格式编码 string,如果出错默认报一个ValueError 的异常,除非 errors 指定的是’ignore’或者’replace’

保存文件名是用encode指定编码格式,再读取时用decode指定格式解码,否则遇到中文字读取会出问题。

起因

今天在移植陀螺仪项目时,之前的代码全局变量过多,函数功能不够独立,现在使用国产雅特力MCU,M4主频120MHz,64K ROM,16K RAM,想使用RTOS重新写代码,实践一次嵌入式RTOS编程。

参考资料

  1. 参考RT-Thread官网教程:https://www.rt-thread.org/document/site/#/

开始干

选型

选用RT-Thread Nano版本,资源占用小:对 RAM 与 ROM 的开销非常小,在支持 semaphore 和 mailbox 特性,并运行两个线程 (main 线程 + idle 线程) 情况下,ROM 和 RAM 依然保持着极小的尺寸,RAM 占用约 1K 左右,ROM 占用 4K 左右。

移植

移植参考官网教程:使用 MDK 移植

移植时遇到的几个问题

  1. #error "TODO 1: OS Tick Configuration."一直报错,这个只是编译时提醒我们要配置OS Tick,配置后需手动注释掉这条。
  2. SysTick_Handler()有时进有时不进,检查方法查看SysTick结构体,发现CTRL中使能位0,原因:后面的代码使用了delay函数关闭SysTick的使能位。

线程

初始化静态线程

1
2
3
4
5
rt_err_t rt_thread_init(struct rt_thread* thread,
const char* name,
void (*entry)(void* parameter), void* parameter,
void* stack_start, rt_uint32_t stack_size,
rt_uint8_t priority, rt_uint32_t tick);

启动线程

rt_err_t rt_thread_startup(rt_thread_t thread);

遇到的问题

  1. 遇到rt_thread_init()函数卡死,百度竟然啥都查不到(rt-thread用的人这么少的吗),后尝试将栈大小增加至256后成功初始化线程。虽然任务里没什么局部变量,但是一个简单的按键任务竟然占了500多字节内存,可能是因为栈中还保存了寄存器、TCB等信息,那我这单片机16K字节内存可能不够。

  2. 需要在main函数while(1)中加入rt_thread_mdelay(10); 否则main线程优先级更高,其他线程无法运行。