初始目的是为了通过汇编编写CRC功能。
但是基础为0,所以目前从搭建工程开始记录。
大佬绕路。
(一)创建项目
1. 新建项目
- 打开 Keil uVision。
- 选择 Project -> New uVision Project 创建一个新项目。
- 选择你的目标设备(如 ARM Cortex-M 系列处理器),我这里一开始选择的M0,后面因为报错改为了M3。
2. 新建汇编文件
- 在项目中,右键点击 Source Group 1,选择 Add New Item to Group ‘Source Group 1’。
- 选择 Assembler Source File,命名文件并点击 Add。
3. 编写汇编代码
```asmAREA MyCode, CODE, READONLYENTRYEXPORT __main__main MOV R0, #0x10 ; 将立即数 0x10 加载到 R0 寄存器MOV R1, #0x20 ; 将立即数 0x20 加载到 R1 寄存器ADD R2, R0, R1 ; 将 R0 和 R1 相加,结果存入 R2B . ; 无限循环END
```
4. 配置启动文件
确保项目中包含正确的启动文件(startup file),这对于 ARM Cortex-M 项目来说非常重要。你可以在目标设备的 Startup 文件夹中找到适合的启动文件并将其添加到项目中。
5. 编译项目
1.点击 Project -> Build Target 或按快捷键 F7 进行编译。
2.如果有编译错误,修复后重新编译。
6. 加载并调试代码
-
配置模拟器:
- 选择 Options for Target ‘Target 1’(点击工具栏中的扳手图标)。
- 在 Debug 选项卡中,选择 Use Simulator。
- 点击 OK 保存配置。
-
进入调试模式:
- 点击 Debug -> Start/Stop Debug Session 或按快捷键 Ctrl+F5 进入调试模式。
- Keil 将启动模拟器,你可以看到一个新的调试窗口。
-
调试代码:
- 使用调试工具栏中的按钮进行代码的单步执行(Step Over/F10, Step Into/F11)、设置断点(在代码行左侧点击)和观察寄存器、内存等。
- 在调试窗口中,可以使用 Watch 窗口查看变量和寄存器的值。
- 使用 Memory 窗口查看特定内存地址的内容。
- 使用 Register 窗口查看和修改 CPU 寄存器的值。
(二)Disassembly窗口解释
-
地址列(Address):
这一列显示了每条汇编指令在内存中的地址。地址是十六进制格式,表示当前指令在程序内存中的位置。这有助于你了解代码在内存中的分布以及调试时的具体位置。 -
机器码列(Machine Code / Opcode):
这一列显示了每条汇编指令对应的机器码(十六进制格式)。它展示了 CPU 执行指令时实际处理的字节码。对于调试和分析底层行为非常有用。 -
汇编指令列(Disassembly / Instruction):
这一列显示了指令的汇编语言格式。它展示了 CPU 执行的指令以及它们的操作数。你可以看到每条指令的操作码和操作数,例如 MOV R0, #1 或 ADD R1, R2。 -
源代码列(Source Code)(如果有):
如果你的工程配置了源代码文件,并且源代码与汇编指令有映射关系,Disassembly 窗口可能会显示源代码行号和代码内容。这列帮助你将汇编代码与对应的高层源代码关联起来。 -
符号列(Symbol)(如果启用):
这列可能显示符号信息,例如函数名或变量名。在某些设置中,你可以看到每条汇编指令的符号名称,这对于理解代码的功能和调试非常有帮助。
Address Machine Code Disassembly Source Code
08000000 E3A00001 MOV R0, #1 // int x = 1;
08000004 E0801003 ADD R1, R0, R3
- Address:08000000 是 MOV R0, #1 指令的内存地址。
- Machine Code:E3A00001 是 MOV R0, #1 指令的机器码。
- Disassembly:MOV R0, #1 是指令的汇编语言表示。
- Source Code:如果有源代码文件,这一列显示了相关的源代码行(如 int x = 1;)。
(三)寄存器窗口
在寄存器窗口中,通常会显示以下信息:
-
寄存器名称(Register Name):
显示寄存器的名称。常见的寄存器名称包括 R0、R1、PC(程序计数器)、SP(栈指针)、LR(链接寄存器)等。不同的微控制器架构可能有不同的寄存器集合。 -
寄存器值(Value):
显示每个寄存器当前的值,通常以十六进制格式表示。这些值是 CPU 当前使用的寄存器内容。
使用寄存器窗口
-
查看寄存器值:
在寄存器窗口中,可以看到所有寄存器的当前值。 -
修改寄存器值:
有些调试器允许手动修改寄存器的值。可以在寄存器值栏中直接输入新的值并按 Enter 键来修改,例如可以修改程序计数器(PC)以改变程序的执行流。 -
同步查看:
寄存器窗口通常会自动更新显示寄存器的最新值。当程序执行或暂停时,可以看到寄存器值的变化。
Register Value
---------------------
R0 0x00000010
R1 0x00000020
R2 0x00000030
PC 0x08000000
SP 0x20001000
LR 0x08000010
R0:寄存器 R0 的值是 0x00000010。
PC:程序计数器 PC 的值是 0x08000000,表示当前正在执行的指令地址。
SP:栈指针 SP 的值是 0x20001000,表示当前栈的顶部地址。
(四)Program Size
Program Size: Code=544 RO-data=268 RW-data=4 ZI-data=3445
Program Size 是指编译和链接后的程序代码和数据在目标微控制器(如 ARM Cortex-M)中所占用的存储空间。它主要由以下几个部分组成:
1. Code Size (代码大小)
这是指程序中所有的指令(即代码段 .text)在目标设备闪存中所占用的空间。它包括用户编写的代码、库函数、以及编译器生成的代码。
影响因素:代码优化级别、函数的复杂度、库函数的使用情况、内联函数(inline functions)等。
2. RO Data Size (只读数据大小)
这是指所有只读数据(即 .rodata 段)在闪存中所占用的空间。这些数据包括常量字符串、只读变量、编译时确定的初始化数据等。
影响因素:程序中定义的常量、字符串以及所有使用 const 修饰的变量。
3. RW Data Size (读写数据大小)
这是指可读写数据段(即 .data 段)在闪存和 SRAM 中所占用的空间。它包括全局变量和静态变量(静态分配的),这些变量在程序启动时由闪存中的初始化数据拷贝到 SRAM 中。
影响因素:全局变量、静态变量的数量和类型,以及这些变量的初始值。
4. ZI Data Size (零初始化数据大小)
这是指未初始化的全局变量和静态变量(即 .bss 段)在 SRAM 中所占用的空间。在程序启动时,这些变量会被初始化为零,因此在闪存中不占用空间。
影响因素:未初始化的全局变量和静态变量的数量和类型。
5. Stack Size (堆栈大小)
堆栈用于存储函数调用时的局部变量、返回地址、中断处理器上下文等。它位于 SRAM 中,但在 Program Size 报告中通常不会直接显示。
影响因素:递归调用深度、函数中局部变量的数量和大小、中断服务程序的嵌套层次等。
6. Heap Size (堆大小)
堆用于动态内存分配(如 malloc),位于 SRAM 中。在 Program Size 报告中通常也不会直接显示,但它的大小会影响 SRAM 的总体使用情况。
影响因素:动态内存分配的需求,程序运行时的内存分配策略。
7. 总大小 (Total Size)
这是代码、只读数据、读写数据和零初始化数据段的总和,通常是需要在闪存和 SRAM 中分配的总空间。对于嵌入式系统,优化代码大小以符合设备的存储限制是非常重要的。
(五)map文件
1. 总空间大小分配
Code (inc. data) RO Data RW Data ZI Data Debug 11720 1356 1268 108 1388 267457 Grand Totals11720 1356 1268 108 1388 267457 ELF Image Totals11720 1356 1268 108 0 0 ROM Totals==========================================================================Total RO Size (Code + RO Data) 12988 ( 12.68kB)
Total RW Size (RW Data + ZI Data) 1496 ( 1.46kB)
Total ROM Size (Code + RO Data + RW Data) 13096 ( 12.79kB)
KEIL编译器会在每次编译和链接后生成一个 map 文件,其中详细列出了程序中每个段的大小,以及每个段在闪存和 SRAM 中的具体分布情况。
RO Size (Code + RO Data) ,表示程序占用Flash空间的大小。RW Size (RW Data + ZI Data),表示运行时占用的RAM的大小。ROM Size (Code + RO Data + RW Data) ,表示烧写程序占用的Flash空间的大小。
2. Memory Map of the image
打开.map文件,找到Memory Map of the image即可查看对应的内存分配。
1. Exec Addr (执行地址)
- 定义:这是程序代码或数据在运行时的执行地址,即程序运行时,该部分代码或数据会被映射到这个地址。
- 用途:确定程序在内存中实际运行的地址,通常用于区分代码的执行位置(如在闪存中)和数据的运行位置(如在 SRAM 中)。
2. Load Addr (加载地址)
- 定义:这是程序或数据在加载时的地址,即当程序加载到内存中时,该段最初被存储的地址。
- 用途:加载地址通常用于初始化数据时。对于代码段,加载地址和执行地址通常是相同的;但对于初始化的数据段,加载地址通常在闪存中,而执行地址在 SRAM 中。
3. Size (大小)
- 定义:表示该段占用的内存大小,通常以字节为单位。
- 用途:用于确定每个段占用了多少内存空间,便于内存布局优化。
4. Type (类型)
- 定义:标识该段的类型,通常包括 Code、Data 等类型。
用途: - Code:表示这部分是代码段。
- Data:表示这部分是数据段(包括初始化数据和未初始化数据)。
5. Attr (属性)
- 定义:标识该段的属性,通常是一个或多个字符组合,如 RO(只读),RW(读写),ZI(零初始化)。
用途: - RO:只读数据,如常量或代码。
- RW:可读写数据,如初始化的全局变量。
- ZI:零初始化数据段,通常是未初始化的全局变量。
6. Idx (索引)
- 定义:这是该段在 MAP 文件中的索引号,用于区分和标识不同的段。
- 用途:可以帮助开发者快速定位和参考特定的段信息。
7. Section Name (段名称)
- 定义:表示该段在源文件中的名称,如 .text(代码段)、.data(数据段)、.bss(未初始化数据段)等。
- 用途:段名称帮助识别该段属于哪个类型和功能,便于调试和优化。
8. Object (对象)
- 定义:表示该段所属的目标文件(通常是 .o 文件),即该段是由哪个源文件生成的。
- 用途:帮助开发者了解代码和数据是从哪个源文件或库生成的,以便进行进一步的分析和优化。
示例解释
假设 MAP 文件中有如下条目:
Exec Addr Load Addr Size Type Attr Idx E Section Name Object
0x08000000 0x08000000 0x00000400 Code RO 1 .text main.o
0x20000000 0x08000400 0x00000020 Data RW 2 .data main.o
0x20000020 0x20000020 0x00000100 Data ZI 3 .bss main.o
-
第1行:
- Exec Addr 和 Load Addr:0x08000000,表示代码段 .text 在加载和执行时都在这个地址(通常是闪存)。
- Size:0x400 字节,即 1024 字节。
- Type:Code,表示这是代码段。
- Attr:RO,表示该段是只读的。
- Section Name:.text,表示这是代码段。
- Object:main.o,表示这是从 main.o 文件中生成的段。
-
第2行:
- Exec Addr:0x20000000,表示这个数据段在执行时位于 SRAM 中。
- Load Addr:0x08000400,表示数据的初始值存储在闪存中(通常在程序启动时会从闪存加载到 SRAM)。
- Size:0x20 字节,即 32 字节。
- Type:Data,表示这是数据段。
- Attr:RW,表示该段是可读写的。
- Section Name:.data,表示这是初始化数据段。
- Object:main.o,表示这是从 main.o 文件中生成的段。
-
第3行:
- Exec Addr 和 Load Addr:0x20000020,表示这个段在 SRAM 中,且未初始化(因此不占用闪存)。
- Size:0x100 字节,即 256 字节。
- Type:Data,表示这是数据段。
- Attr:ZI,表示这是零初始化段。
- Section Name:.bss,表示这是未初始化数据段。
- Object:main.o,表示这是从 main.o 文件中生成的段。
(六)keil的memory中颜色的区别
KEIL IDE 中,Memory 视图使用颜色来表示不同类型的内存区域或数据。
1. 黄色 (Yellow)
- 含义:黄色通常表示 已分配的代码段 或 只读数据。
- 用途:在 Flash 中,这通常是代码 (.text 段) 或者是只读常量数据 (.rodata 段),这些内容在程序运行时不会被修改。
- 例子:程序的指令集、常量字符串、const 变量等。
2. 绿色 (Green)
- 含义:绿色通常表示 已分配的可读写数据段。
- 用途:在 SRAM 中,这通常是已初始化的全局变量和静态变量 (.data 段),以及动态分配的内存(如堆或栈)。
- 例子:全局变量、静态变量、初始化的数组或结构体等。
3. 黑色 (Black):
- 含义:表示未使用或未分配的内存空间。
- 用途:在这种区域内存并未被当前的程序占用,可能在某些情况下可用于动态分配(如堆的扩展)或供其他用途。
- 位置:这种颜色通常出现在内存映射中未被分配的区域,包括 Flash、SRAM 等。
总结
- 黄色:表示代码或只读数据,通常位于 Flash 中,不会在运行时改变。
- 绿色:表示可读写的数据段,通常位于 SRAM 中,可以在运行时修改。
- 黑色:表示未分配的内存区域,内存视图中这些部分是空闲的,没有被程序使用或初始化。
颜色变化
在 KEIL 的内存视图中,黄色 和 绿色 的变化通常与内存的使用状态和访问权限有关。虽然黄色通常表示只读内容,如代码段或只读数据段,但有时候这些段会在程序运行时被修改,导致它们的状态发生变化。这种变化可能反映为颜色从黄色变为绿色。
可能的原因包括:
- 内存重定位或重写:
某些情况下,代码或只读数据段在运行时可能会被重新加载或重写。例如,某些嵌入式系统中,可能会使用自编程技术动态修改程序的某些部分,这可能导致这些段的状态从只读变为可写。 - 调试器的视角:
在调试过程中,调试器可能会修改某些内存区域,特别是在使用断点或修改内存时。调试器可能将某些内存段标记为可写(绿色),即使它们在正常情况下是只读的(黄色)。这种情况尤其常见于调试初始化数据或调试代码时。 - 运行时状态改变:
一些系统在启动时会将某些内存段从只读状态转换为可写状态。例如,启动时初始化数据段可能会从闪存(只读,黄色)复制到 SRAM(可写,绿色),以便程序运行时修改这些数据。
调试工具的内存可视化更新:
在调试过程中,内存视图会动态更新,显示当前内存的状态。如果调试器检测到某段内存的访问权限发生了变化(例如从只读变为读写),颜色也会相应更新。
问题记录
1. error: A1859E: Flag preserving form of this instruction not available
解决方法
(1)MOV等命令加上s
(2)更改为M3(一开始使用的M0)
2.Error: L6218E: Undefined symbol Image$$ARM_LIB_STACK$$ZI$$Limit (referred form startup_armcm3.o).
因为 armclang 默认不勾选 Use Memory Layout from Target Dialog 选项,因此必须要手动添加链接脚本,即分散加载文件。通过下拉框,直接使用默认的链接脚本即可。
这里记得选择M3对应的。图片是借鉴的其他人的。
3.不同group见互相include文件失败
选择 Options for Target。
配置包含路径:
- 在弹出的对话框中,选择 C/C++ 选项卡。
- 在 Include Paths 输入框中添加包含头文件的路径。
3. error 65: access violation at xxx : no 'read' permission
通常表示汇编代码中尝试访问未授权或不存在的内存区域。这个错误常见于访问无效的地址或指令地址不正确。
检查内存访问
-
访问的地址是否有效:
确保你读取和写入的内存地址在有效范围内。特别是访问寄存器或数据结构时,确保地址正确。 -
栈操作:
如果你从栈中读取参数,确保栈指针 SP 的位置正确。访问栈空间时,地址应该是相对于栈指针的正确偏移量。
4.Note: source file ‘..\xx\cxx\xx.c‘ -object file renamed from “xx.o“ to “xx_1.o“
原因
使用keil添加文件时,在不同的group文件夹里添加了两个相同的.c文件,会导致编译出现如上的提示,即同一个c源文件,在不同的文件夹下,被添加了多次。
解决办法
(1)反复rebuild
- 删除重复文件
- 在对应重复的文件上右键“Options for File ‘xxx.c’…”,取消"include in Target Build"处的勾选,点击“OK”后,rebuild
这一步不管报错与否都不用管 - 再次勾选上,rebuild
虽然这个方法没有解决我的问题,但是很多人好像可以解决,用以参考
(2)重命名
我的group下,.c,.h.s都是一个名字,更改.s名字后不报错
5.关于寄存器读取字节顺序问题
我做到cr64时,需要分别读取内存中的高 32 位和低 32 位的 CRC 多项式。然而,读取的顺序可能会受到系统的字节序影响。
字节序 (Endianness) 是决定多字节数据在内存中存储顺序的系统特性,有两种常见的字节序:
- 大端序 (Big-endian): 高字节存储在低地址,低字节存储在高地址。
- 小端序 (Little-endian): 低字节存储在低地址,高字节存储在高地址。
ARM 处理器可以支持这两种字节序,但默认情况下大多数 ARM 设备使用小端序。这意味着在内存中存储的数据顺序可能与我们在代码中预期的顺序相反。
- 假设你的系统是小端序
例如,如果内存中存储的 64 位数0x42F0E1EBA9EA3693
是按小端序排列的,它在内存中的布局将是:
内存地址: 低地址 --> 高地址
数据: 93 36 EA A9 EB E1 F0 42
6.不能用M3的核,必须用M0的,接下来记录改为M0的一些修改
6.1 Error: 1874E: Specified register list cannot be loaded or stored in target instruction setxxx
这个问题由于我PUSH和POP超过了R4-R7,改为PUSH{R4-R7, LR}
即可
pop的时候也要注意,不能直接popLR
需要
POP {R4-47}
POP {R3}
MOV PC, R3
6.2其他寄存器使用问题
用M0的核的话,大部分的指令只能访问R4-R7,少部分指令如MOV可访问高位寄存器R8-R12.这里整个重新规划寄存器的使用
6.3指令变体
Cortex-M0架构中,某些指令的变体(如设置条件标志的变体)可能不可用,例如ADD指令在设置条件标志时可能需要用ADDS。
ADDS、MOVS、SUBS、LSLS、ANDS和ORRS指令:这些指令不仅执行算术或逻辑操作,还会根据结果自动更新条件标志位。使用这些指令可以替代标准的ADD、MOV、SUB、LSL、AND和ORR指令,以确保符合Cortex-M0架构的指令集。
如MOV立即数需要用MOVS,而寄存器则仍然可以使用MOV
6.4 LDRB自增
如LDRB R4, [R5], #1
数据读取与地址更新分离:
将LDRB R4, [R5], #1
指令分成两步:
- 先用
LDRB R4, [R5]
读取字节 - 再用
ADDs R5, R5, #1
更新指针R5。
这样可以避免在Cortex-M0架构中出现的寻址模式不支持问题。
6.5 BIC R0, R0, #0x100
清零问题
Cortex-M0架构不支持BIC指令直接处理立即数超过8位。
对于BIC R0, R0, #0x100
这行代码,可以通过逻辑与的方式替换为:
ANDS R0, R0, #0xFF ; 保留低8位,将高位清零
这行指令会将R0的高位清零,仅保留低8位。这样做可以达到与BIC R0, R0, #0x100相同的效果,同时适用于Cortex-M0架构。
6.6 CRC16的最高位清零问题
immediate 0x0000ffff out of range for this operation,permintted values are 0x00000000 to 0x000000FF
在ARM的Thumb指令集中(比如Cortex-M0),ANDS指令只允许使用8位以内的立即数(0x00到0xFF)。但是尝试使用的立即数是0xFFFF,超出了这个范围,所以会报错。
可以通过先用MOVS指令将0xFFFF加载到一个寄存器,然后再使用ANDS指令来解决这个问题。
MOVS R2, #0xFF ; 将0xFF加载到R2
LSLS R2, #8 ; 将R2左移8位,得到0xFF00
ADDS R2, R2, #0xFF ; 加上0xFF,得到0xFFFF
ANDS R0, R2 ; 将R0和R2进行按位与运算
6.7 LDRB R5, [R4, #48],m0核报错,data transfer offset 0x00000030out of range,permitter values are 0x00000000 to 0x0000001F
在 ARM Cortex-M0 架构中,数据传输指令(如 LDRB)的偏移量限制在 5 位以内,也就是 0x00 到 0x1F(十进制 0 到 31)。当偏移量超过这个范围时,会报错。
如果需要使用更大的偏移量,可以通过多次累加的方式来实现。比如,可以先将偏移量分成多步,或者先将基址寄存器加上偏移量,再执行加载指令。
解决方法
1. 分段加偏移:
可以先对寄存器进行加法运算,然后再使用 LDRB 指令。
ADDS R4, R4, #32 ; 先加32,使偏移量小于32
LDRB R5, [R4, #16] ; 现在的偏移量是16,总偏移量是48
2. 直接调整基址寄存器:
可以将偏移量直接添加到基址寄存器,然后进行加载操作。
ADDS R4, R4, #48 ; 将R4加上48,偏移基址
LDRB R5, [R4, #0] ; 使用偏移量为0的LDRB指令
6.8 复合语句拆分
ORR R0, R0, R1, LSR #31
这种形式的操作可能会报错,因为Cortex-M0处理器在Thumb指令集中不支持使用立即数位移的形式。可以通过分步操作来实现这一功能。具体实现如下:
LSRS R1, R1, #31 ; 将R1逻辑右移31位,将结果存入R1
ORRS R0, R0, R1 ; 将R0和R1按位或,结果存入R0
7. 无法查看汇编返回结果
如果result是局部变量,有时调试器可能不容易显示其值。可以将result声明为全局变量来确保它在调试器中容易被访问。
volatile int result; // 全局变量,确保在调试器中可见int main() {result = add(3, 4); // 调用汇编函数while(1); // 用于暂停程序在此处,方便调试器检查变量值
}
参考链接
以上很多图片由于keil不在当前电脑,图片大部分借用的其他人的。
参考链接1
参考链接2
参考链接3
参考链接4
参考链接5