org 0x7c00解疑

问:为什么编写NASM语法的系统引导程序,汇编代码的开始总是使用“org 7c00h”?为什么有时候去掉org指令程序也能正常执行?
      答:对于这个问题,我首先在《NASM中文手册》中找到了org指令的解释:NASM汇编编译器为bin文件格式提供了额外的操作符org,它的功能是指定程序被载入内存时的起始地址。根据书中的解释,我们很容易想到,因为引导程序将会被加载到内存0x7c00处,而且引导程序一般都被编译成bin文件格式(bin文件格式没有文件头,它的文件映像与加载到内存运行时的内存映像是一致的),似乎在引导程序中用 org 7c00h是很符合规范的,可仔细一想,似乎又不对,按《NASM中文手册》的说法,使用org 7c00h 将会指定程序加载入内存的起始地址为7c00h,但我们都知道,引导程序加载到内存的7c00h处是一项标准,并不是在编程时决定的,经过试验也验证了我 的想法,将org后的数字改成其他值,bois程序一样将它加载到7c00处。那么是不是可以去掉org指令呢,因为程序被加载到哪里并不关它什么事。于 是我将org指令去掉,重新编译,写入软盘引导扇区,用它引导系统。出乎预料,程序不能正常运行——没有正常打印出提示信息!到底怎么回事呢?我决定一探 究竟。以下是我的引导代码:

boot.asm
1    %include "PrintLib.inc"
2    
3     org 07c00h
4     ;org 0100h
5     mov ax,cs
6     mov ds,ax
7     mov es,ax
8    
9     mov ah,10h
10     mov al,03h
11     mov bl,01h
12     int 10h
13    
14     PrintString BootMessage,LenOfBootMessage,display_mode_2,0h,(ATTR_BLACK<<4)|ATTR_GREEN,0000h
15    
16     hlt ;停机
17    
18    
19     BootMessage: db "Dreamix Starting Please wait..."
20     LenOfBootMessage equ $-BootMessage
21    
22     times 510-($-$) db 0
23     dw 0xaa55

         boot.asm 里引用到了PrintLib.inc文件,PrintLib.inc文件中定义了一个向屏幕输出字符串信息的宏,封装了部分10h BISO子功能,文件内容如下:

PrintLib.inc
1     %ifndef PrintLib
2         %define PrintLib
3    
4    ;此宏在实模式下使用,属于BIOS子功能调用
5    
6    ;显示模式
7    %define display_mode_1 00h ;字符串只包含字符码,显示之后不更新光标位置,属性值在BL中
8    %define display_mode_2 01h ;字符串只包含字符码,显示之后更新光标位置,属性值在BL中
9    %define display_mode_3 02h ;字符串包含字符码及其属性值,显示之后不更新光标位置
10   %define display_mode_4 03h ;字符串包含字符码及其属性值,显示之后更新光标位置
11    
12    ;背景及字体格式属性值
13    %define ATTR_BLACK   0h
14    %define ATTR_BLUE    01h
15    %define ATTR_GREEN   02H
16    %define ATTR_PURPLE   03h
17    %define ATTR_RED     04h
18    %define ATTR_MAGENTA   05h
19    %define ATTR_BROWN   06h
20    %define ATTR_GREYISH   07h
21    %define ATTR_GREY    08h
22    %define ATTR_LIGHTBLUE 09h
23    %define ATTR_LIGHTGREEN 0Ah
24    %define ATTR_LIGHTPURPLE 0Bh
25    %define ATTR_LIGHTRED  0Ch
26    %define ATTR_LIGHTMAGENTA 0Dh
27    %define ATTR_YELLOW   0Eh
28    %define ATTR_WHITE  0Fh
29    
30    ;参数: 1.要显示的字符串标号  2.要显示的字符串的长度值
31    ; 3.显示模式
32    ; 4.视频页号
33    ; 5.当显示模式选3和4时为0h,否则需要背景和字符的格式属性值
34    ; 6.显示的列和行
35    %macro PrintString 6
36    
37     push ax
38     push bp
39     push cx
40     push bx
41     push dx
42    
43     mov ax,%1
44     mov bp,ax
45     mov cx,%2
46     mov ax,01300h + %3
47     mov bx,%4 + %5
48     mov dx,%6
49     int 10h
50    
51     pop dx
52     pop bx
53     pop cx
54     pop bp
55     pop ax
56    %endmacro
57    
58    %endif  

        以上两个文件中的代码都是正确的代码,其中的boot.asm文件使用了org 7c00h.当我把org 7c00h语句去掉后,编译的引导程序不能正常打印提示信息"Dreamix Starting Please wait...".

        用WinHex查看编译好的boot.bin文件,显示如下:

Offset      0  1  2  3  4  5  6  7   8  9  A  B  C  D  E  F
00000000   8C C8 8E D8 8E C0 B4 10  B0 03 B3 01 CD 10 50 55   ???????.°.?.?.PU
00000010   51 53 52 B8 2C 7C 89 C5  B9 1F 00 B8 01 13 BB 02   QSR?,|‰??..?..?.
00000020   00 BA 00 00 CD 10 5A 5B  59 5D 58 F4 44 72 65 61   .?..?.Z[Y]X?Drea
00000030   6D 69 78 20 53 74 61 72  74 69 6E 67 20 50 6C 65   mix Starting Ple
00000040   61 73 65 20 77 61 69 74  2E 2E 2E 00 00 00 00 00   ase wait........
00000050   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000060   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000070   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000080   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000090   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
000000A0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
000000B0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
000000C0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
000000D0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
000000E0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
000000F0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000100   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000110   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000120   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000130   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000140   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000150   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000160   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000170   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000180   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000190   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
000001A0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
000001B0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
000001C0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
000001D0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
000001E0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
000001F0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 55 AA   ..............U?

         可以看到字符串被编译到文件头偏移02Ch处,此时文件映像的内容也反映了它在内存中的映像内容,当boot.bin被加载到内存7c00h处时,字符串 "Dreamix Starting Please wait..."应该位于内存7c2ch处。我们用Bochs调试未加org 7c00语句的引导程序,看看它到底是如何运行的。将boot写入软盘镜像文件,把该镜像文件挂载为Bochs虚拟机的软驱,启动Bochs调试器,先用 pb命令在物理内存0x7c00处下断点,然后用c命令执行执行到断点(关于如何用Bochs调试操作系统请读者自己查阅相关资料,此处不再赘述),此时 boot.bin已经被加载到内存0x7c00处。用disassemble命令反汇编这段内存,得到如下结果:

<bochs:3> disassemble 0x7c00 0x7c30
00007c00: (                    ): mov ax, cs                ; 8cc8
00007c02: (                    ): mov ds, ax                ; 8ed8
00007c04: (                    ): mov es, ax                ; 8ec0
00007c06: (                    ): mov ah, 0x10              ; b410
00007c08: (                    ): mov al, 0x3               ; b003
00007c0a: (                    ): mov bl, 0x1               ; b301
00007c0c: (                    ): int 0x10                  ; cd10
00007c0e: (                    ): push ax                   ; 50
00007c0f: (                    ): push bp                   ; 55
00007c10: (                    ): push cx                   ; 51
00007c11: (                    ): push bx                   ; 53
00007c12: (                    ): push dx                   ; 52
00007c13: (                    ): mov ax, 0x2c              ; b82c00
00007c16: (                    ): mov bp, ax                ; 89c5
00007c18: (                    ): mov cx, 0x1f              ; b91f00
00007c1b: (                    ): mov ax, 0x1301            ; b80113
00007c1e: (                    ): mov bx, 0x2               ; bb0200
00007c21: (                    ): mov dx, 0x0               ; ba0000
00007c24: (                    ): int 0x10                  ; cd10
00007c26: (                    ): pop dx                    ; 5a
00007c27: (                    ): pop bx                    ; 5b
00007c28: (                    ): pop cx                    ; 59
00007c29: (                    ): pop bp                    ; 5d
00007c2a: (                    ): pop ax                    ; 58
00007c2b: (                    ): hlt                       ; f4
00007c2c: (                    ): inc sp                    ; 44
00007c2d: (                    ): jb .+0x7c94               ; 7265
00007c2f: (                    ): popa                      ; 61

        实际上可执行的代码只到00007c2b,后面的是数据和填充字符,我们看到,红色标出的两条指令负责在调用10h中断之前将需要打印的字符串的首地址装入约定的寄存器bp,它装入的是字符串相对于7c00的偏移0x2c。我们知道,实模式下内存的寻址是通过段寄存器提供的段基址和偏移地址相组合的方式,这里的0x2c相当于偏移地址。那么段基址是多少呢?我们在Bochs下用info registers命令查看,得到以下结果:

<bochs:7> info registers
eax            0xfffaa55        268413525
ecx            0xa0001          655361
edx            0x0              0
ebx            0x0              0
esp            0xfffe           0xfffe
ebp            0x0              0x0
esi            0xa070           41072
edi            0xffde           65502
eip            0x7c00           0x7c00
eflags         0x82             130
cs             0x0              0
ss             0x0              0
ds             0x0              0
es             0x0              0
fs             0x0              0
gs             0x0              0

         一般来说bp是进行堆栈寻址的,我不知道此时bp是和哪个段寄存器配合寻址,但我们发现所有6个段寄存器的值都是0,因此无论如何(即使引导程序一开始就 将cs寄存器的内容复制到了es和ds),用bp里的偏移0x2c和段基址0x0只能访问到内存单元0x2c,而不是0x7c2c(字符串 "Dreamix Starting Please wait..."被加载大内存0x7c2c处),这就是为什么程序不能正确打印提示字符的原因。受原来编程习惯的影响,我们想当然的认为段寄存器应该存着 程序被加载到的段的基址,也就是0x7c00,这样和偏移地址0x2c结合刚好可以正确找到字符串,但事实是引导程序被加载后,所有的段寄存器都被清零 了,不知道这是不是IBM PC兼容机启动时的规范动作?知道的朋友可以给我留言。此时,由于段寄存器的值为0x0,偏移地址就变成了绝对(物理)地址,要访问内存0x7c2c,偏移地址就必须是0x7c2c;或者编程时将段寄存器的值赋为0x7c00,偏移地址0x2c保持不变,也可以达到预期目的。

        我们来看看boot.asm中加入org 7c00h指令后的情况,按前述步骤在Bochs中反汇编代码,结果如下:

<bochs:5> disassemble 0x7c00 0x7c30
00007c00: (                    ): mov ax, cs                ; 8cc8
00007c02: (                    ): mov ds, ax                ; 8ed8
00007c04: (                    ): mov es, ax                ; 8ec0
00007c06: (                    ): mov ah, 0x10              ; b410
00007c08: (                    ): mov al, 0x3               ; b003
00007c0a: (                    ): mov bl, 0x1               ; b301
00007c0c: (                    ): int 0x10                  ; cd10
00007c0e: (                    ): push ax                   ; 50
00007c0f: (                    ): push bp                   ; 55
00007c10: (                    ): push cx                   ; 51
00007c11: (                    ): push bx                   ; 53
00007c12: (                    ): push dx                   ; 52
00007c13: (                    ): mov ax, 0x7c2c            ; b82c7c
00007c16: (                    ): mov bp, ax                ; 89c5
00007c18: (                    ): mov cx, 0x1f              ; b91f00
00007c1b: (                    ): mov ax, 0x1301            ; b80113
00007c1e: (                    ): mov bx, 0x2               ; bb0200
00007c21: (                    ): mov dx, 0x0               ; ba0000
00007c24: (                    ): int 0x10                  ; cd10
00007c26: (                    ): pop dx                    ; 5a
00007c27: (                    ): pop bx                    ; 5b
00007c28: (                    ): pop cx                    ; 59
00007c29: (                    ): pop bp                    ; 5d
00007c2a: (                    ): pop ax                    ; 58
00007c2b: (                    ): hlt                       ; f4
00007c2c: (                    ): inc sp                    ; 44
00007c2d: (                    ): jb .+0x7c94               ; 7265
00007c2f: (                    ): popa                      ; 61

        可以看到,加入org 7c00h后,偏移被编译成了0x7c2c,再次用info registers命令查看寄存器内容,发现还是全为0x0,因此程序通过基址加偏移的方式能够正确访问到字符串。

        通过以上分析,我们可以看出org指令的作用确实是指示出程序将要被加载到内存的起始地址,这里用“指示”比用原来的“指定”更确切点。“指示”有被动的 含义,org指令本身并不能决定程序将要加载到内存的什么位置,它只是告诉编译器,我的程序在编译好后需要加载到xxx地址,所以请你在编译时帮我调整好 数据访问时的地址。用“指定”有种主动的含义,容易引起误解。另外我们看出,org指令只会在编译期影响到内存寻址指令的编译(编译器会把所有程序用到的 段内偏移地址自动加上org后跟的数值),而其自身并不会被编译成机器码。

        到此我们已经初步了解到了org指令的作用,一句话,就是为程序中所有的内部地址引用增加一个段内偏移值(引导程序可以看做是被加载到以0为基址的段,偏移为0x7c00)。

        为了方便调试,我们常常用NASM将 程序编译为windows能直接执行的com文件,这样能在windows下直接运行观看其效果。其实com文件格式和bin文件格式并没有本质的区别, 它们都是纯二进制文件,即文件映像的内容和内存映像的内容相同。所不同的是,com文件总是被MS的操作系统加载到段偏移0100h处,因此com的汇编 源文件中需要加入语句org 100h。

        下面我们来看看com文件的寻址情况,我们分别用windows自带的debug和boland的turbo debuger 调试运行com版本的boot程序,分析其中的异同:


图1: Debug下调试执行boot.com


图2: Turbo Debuger下调试执行boot.com

        从图1和图2中可以看出,访问字符串的偏移地址都是012ch,即org指令指定的100h与字符串文件内偏移相加而得到的值。而两种情况下段寄存器的值 都不为0,一个是0BE2,一个是5328。为什么不同的调试器在加载同一个com文件时,加载到的段各不相同;而同一个调试器每次加载同一文件总在一个 段,这其中有什么规则,我不得而知,请知道的朋友留言告诉我。我们看到,虽然两种情况下的段基址各不相同,但用基址+偏移的方式总能正确访问到内存数据, 这归功于程序总被加载到段偏移0100h处,由此引出了以下结论:

        如果一个程序使用了org xxx指令,那么该程序只能被加载到段内偏移xxx处,否则将不能正常访问段内数据,这是本篇文章得出的最重要的结论。

        最后我们来解释剩下的问题,为什么有时候去掉org指令程序也能正常执行?实际上不加org就相当于没有指示出程序加载的段内偏移值,这时编译器会默认用 0做为偏移(相当于org 0x0的情况)。这样的程序只要加载到段内偏移0x0处都能正常执行,还有一种情况就是程序中没有对内存的寻址操作,那么也不会出错,因为org指令就是 调整段内地址引用值的。
<think>根据用户需求,我们需要实现以下功能: 1. 使用STM32F070F6P6芯片,标准库。 2. 将初始参数存储在Flash中(使用最后一页,地址0x08007C00)。 3. 通过串口1接收数据,接收的数据格式为特定的字节数组(用户提供的一个示例数组,但实际应按照协议格式判断)。 4. 判断接收的数据格式是否匹配(这里用户没有明确格式,但根据提供的示例数据,我们可以设计一个简单的协议:例如,以特定起始字节和结束字节,或者使用固定长度并包含校验和)。 5. 如果格式匹配,则用接收的数据替换初始参数,并存储到Flash中;否则,保留初始参数。 由于用户没有明确给出数据格式的规则,但提供了一个示例数据(长度为58字节的数组),我们可以设计如下协议: - 数据包固定长度为58字节。 - 数据包以0x00,0x00,0x25,0x80开头(根据示例数据的前4个字节)?但是这样不够通用,因为用户可能希望每次发送的数据内容可以变化。 - 或者,我们可以设计一个简单的帧结构:起始标志、数据、校验和、结束标志。 考虑到用户提供的示例数据,我们设计一个简单的协议: [起始标志2字节:0xAA, 0x55] [54字节数据] [校验和1字节] [结束标志1字节:0x55] 这样总长度=2+54+1+1=58字节。 但是,用户提供的示例数据并没有这样的结构,所以我们需要按照用户示例数据的格式来?用户要求是“判断数据格式是否匹配”,但没有给出匹配规则。 根据用户问题中的描述:“如果接收的数据格式不匹配”,那么格式的规则是什么?由于用户没有明确,我们假设格式规则为: - 数据包长度固定为58字节。 - 数据包的最后4个字节必须是0x7C,0x7C,0x7C,0x7C(根据示例数据的倒数第5到倒数第2字节是0x7C,但示例数据最后是0x55?)——实际上示例数据最后是0x17,0x02。 这显然不一致。因此,我们需要用户明确格式规则,但用户没有。这里我们只能根据用户提供的示例数据来假设: 用户示例数据:58个字节,我们注意到: 前4字节:0x00,0x00,0x25,0x80 中间数据:... 最后几字节:...0x7C,0x7C,0x7C,0x7C,0x05,0x40,0x00,0x23,0x00,0x00,0x00,0x3C,0x3C,0x00,0x0A,0x19,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x17,0x02 没有明显的起始和结束标志。因此,我们只能采用固定长度和校验和的方式。 我们设计协议如下: - 数据包固定58字节。 - 校验和:将前57字节相加,取低8位,与第58字节比较。 这样,如果接收58字节,计算前57字节的和的低8位等于第58字节,则认为格式匹配。 但是用户提供的示例数据第58字节是0x02,我们计算前57字节的和: 由于数据较长,我们可以在代码中计算。因此,我们按照这个规则来。 步骤: 1. 初始化:从Flash读取参数,如果Flash中存储的参数无效(例如没有魔数标记),则使用默认参数并写入Flash。 2. 串口接收:使用中断方式接收,或者使用轮询方式(这里为了简单,使用轮询方式在主循环中接收)。 3. 接收数据:当接收到58字节后,进行校验和计算。 4. 校验和正确则认为格式匹配,将整个58字节存储到Flash(替换原来的参数);否则,不更新参数。 注意:由于参数数据是58字节,而我们的Flash存储结构需要按32位字写入,因此我们设计一个结构体来存储这些参数,并包含魔数用于验证。 定义参数存储结构体(60字节,为了按4字节对齐,实际我们存储60字节): typedef struct { uint32_t magic; // 魔数,例如0x55AA55AA uint8_t data[58]; // 参数数据 uint8_t checksum; // 存储时计算的校验和(可选,但这里我们不需要,因为魔数已经可以判断是否有效) } ParamStruct; 但是,这样整个结构体大小为4+58+1=63字节,不够对齐。我们可以将data改为60字节,但用户要求存储58字节。因此,我们这样设计: typedef struct { uint32_t magic; // 4字节 uint8_t data[58]; // 58字节 } ParamStruct; // 总大小62字节,但Flash按字写入,所以我们需要按4字节对齐,实际存储64字节(因为62不是4的倍数,所以补2个字节) 但我们也可以将58字节分成两部分:前56字节(14个字)和最后2字节,然后存储时按字存储。为了简单,我们直接存储60字节(包括魔数4字节+58字节数据,然后在后面补2个字节,凑成64字节?),但这样浪费空间。 另一种方法:我们存储的时候,将整个结构体按4字节对齐,用60字节(15个字)存储,其中魔数占4字节,数据占58字节,后面补2个无用字节(但这样在读取时直接读取58字节数据即可)。 考虑到简单性,我们这样定义: #define PARAM_DATA_SIZE 58 typedef struct { uint32_t magic; uint8_t data[PARAM_DATA_SIZE]; // 不需要显式补字节,因为编译器会自动对齐,但为了确保大小是4的倍数,我们可以用__packed或者手动补齐。 } __attribute__((packed)) ParamStruct; 但是,这样结构体大小是4+58=62字节,不是4的倍数,写入Flash会麻烦。因此,我们手动补齐: typedef struct { uint32_t magic; uint8_t data[60]; // 多分配2个字节,但我们只使用前58个字节,这样结构体大小64字节(4+60=64,实际60字节是15个字?不对,4+60=64字节,16个字?) } ParamStruct; 这样,我们存储60字节,但实际有效数据58字节,后面2个字节不使用。这样在接收时,我们接收58字节,然后存储时,我们将这58字节放到data的前58字节,后面2字节可以置0。 但是,用户要求存储的是58字节,所以我们还是希望精确存储58字节。因此,我们使用packed,然后写入时按字节写入?但Flash只能按字写入。 因此,我们这样处理: 存储时,我们将58字节的数据和4字节的魔数组合成一个62字节的缓冲区,然后按字写入(需要15.5个字?不行,必须整数个字)。所以,我们补2个字节,构成64字节(16个字)。 定义: typedef struct { uint32_t magic; // 4字节 uint8_t data[58]; // 58字节 uint8_t padding[2]; // 2字节填充,使结构体大小为64字节(4的倍数) } ParamStruct; 这样,结构体大小就是64字节。 存储到Flash时,我们直接写入这个结构体(64字节)。 步骤: 1. 初始化时,从Flash地址PARAM_ADDR读取64字节到ParamStruct结构体。 2. 检查magic是否为预设值(例如0x55AA55AA),如果是,则参数有效;否则,使用默认参数(可能是全0,或者预先设定的一组默认参数),并写入Flash。 3. 串口接收58字节数据,计算校验和(前57字节和取低8位,与第58字节比较)。 4. 如果校验通过,则准备一个新的ParamStruct结构体,设置magic为0x55AA55AA,将58字节数据拷贝到data,padding填充0。然后擦除Flash页,写入新的结构体。 5. 如果校验不通过,则忽略,继续使用原来的参数。 注意:由于串口接收58字节,我们使用轮询方式接收,可能会影响其他任务,因此在实际应用中建议使用中断+环形缓冲区。但这里为了简单,我们使用轮询。 我们使用串口1,波特率115200。 代码实现: 步骤: - 初始化系统时钟(使用内部HSI) - 初始化串口1 - 初始化Flash,读取当前参数 - 主循环:轮询串口接收,当接收到58字节时,进行校验,校验通过则更新Flash。 注意:由于Flash写入时间较长,期间要避免中断干扰,因此我们在写入前关闭中断,写完后开启。 但是,标准库操作Flash时,一般要求关闭中断。 代码结构: 1. 定义参数结构体和存储地址 2. 函数:读取参数 3. 函数:写入参数(擦除页,然后写入) 4. 串口初始化 5. 主函数:初始化,读取参数,然后循环接收串口数据 由于轮询接收58字节会阻塞,我们改为非阻塞方式:每次循环读取一个字节,凑够58字节再处理。 具体实现: 我们使用一个缓冲区来接收串口数据,并记录接收计数。 注意:如果接收过程中出现错误(如校验失败),则丢弃当前接收的数据,重新开始接收。 代码示例:</think>以下是一个完整的STM32F070F6P6实现方案,基于标准库实现串口数据接收、格式校验及Flash存储功能: ```c #include "stm32f0xx.h" #include "stm32f0xx_flash.h" #include "stm32f0xx_usart.h" #include "stm32f0xx_rcc.h" #include "stm32f0xx_gpio.h" #include <string.h> // Flash配置 #define FLASH_PAGE_SIZE (0x400) // 1KB页大小[^2] #define PARAM_FLASH_ADDR (0x08007C00) // 使用最后一页存储参数 #define MAGIC_NUMBER (0x55AA55AA) // 魔数验证标识 // 串口配置 #define USART_BAUDRATE 115200 #define RX_BUFFER_SIZE 64 // 参数结构体定义(确保4字节对齐) typedef struct { uint32_t magic; // 魔数验证标识 uint8_t data[58]; // 参数数据区(58字节) uint8_t padding[2]; // 填充字节(使结构体大小为64字节) } __attribute__((aligned(4))) ParamStruct; // 全局变量 volatile uint8_t rxBuffer[RX_BUFFER_SIZE]; volatile uint16_t rxIndex = 0; ParamStruct deviceParams; // 串口初始化 void USART1_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; USART_InitTypeDef USART_InitStruct; // 启用时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); // 配置TX(PA9)和RX(PA10) GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStruct.GPIO_OType = GPIO_OType_PP; GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP; GPIO_Init(GPIOA, &GPIO_InitStruct); // 复用功能映射 GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_1); GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_1); // USART配置 USART_InitStruct.USART_BaudRate = USART_BAUDRATE; USART_InitStruct.USART_WordLength = USART_WordLength_8b; USART_InitStruct.USART_StopBits = USART_StopBits_1; USART_InitStruct.USART_Parity = USART_Parity_No; USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, &USART_InitStruct); // 启用USART接收中断 USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); NVIC_EnableIRQ(USART1_IRQn); USART_Cmd(USART1, ENABLE); } // 从Flash读取参数 void Read_Params(void) { // 直接拷贝Flash内容到结构体 memcpy(&deviceParams, (void*)PARAM_FLASH_ADDR, sizeof(ParamStruct)); } // 写入参数到Flash FLASH_Status Write_Params(void) { FLASH_Status status = FLASH_COMPLETE; // 解锁Flash FLASH_Unlock(); FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_WRPERR | FLASH_FLAG_PGERR); // 擦除目标页 status = FLASH_ErasePage(PARAM_FLASH_ADDR); if(status != FLASH_COMPLETE) return status; // 按字(32位)写入数据 uint32_t *src = (uint32_t*)&deviceParams; for(int i = 0; i < sizeof(ParamStruct)/4; i++) { status = FLASH_ProgramWord(PARAM_FLASH_ADDR + i*4, src[i]); if(status != FLASH_COMPLETE) break; } FLASH_Lock(); return status; } // 数据格式校验函数 uint8_t Validate_DataFormat(uint8_t* data) { // 示例校验规则1:检查特定位置的值 if(data[2] != 0x25 || data[5] != 0x25) return 0; // 示例校验规则2:检查结束标志 if(data[56] != 0x17 || data[57] != 0x02) return 0; // 示例校验规则3:校验和验证 uint8_t checksum = 0; for(int i = 0; i < 57; i++) { checksum += data[i]; } if(checksum != data[57]) return 0; return 1; // 所有校验通过 } // USART1中断服务函数 void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { // 读取接收到的字节 uint8_t rxByte = USART_ReceiveData(USART1); // 存储到缓冲区 if(rxIndex < RX_BUFFER_SIZE) { rxBuffer[rxIndex++] = rxByte; } // 收到完整数据包(58字节) if(rxIndex >= 58) { // 验证数据格式 if(Validate_DataFormat((uint8_t*)rxBuffer)) { // 更新参数结构 deviceParams.magic = MAGIC_NUMBER; memcpy(deviceParams.data, rxBuffer, 58); // 写入Flash if(Write_Params() == FLASH_COMPLETE) { USART_SendData(USART1, 'O'); // 发送确认信号 } else { USART_SendData(USART1, 'E'); // 写入错误 } } else { USART_SendData(USART1, 'F'); // 格式错误 } rxIndex = 0; // 重置接收索引 } } } // 系统时钟初始化 void SystemClock_Init(void) { RCC_HSICmd(ENABLE); while(RCC_GetFlagStatus(RCC_FLAG_HSIRDY) == RESET); RCC_SYSCLKConfig(RCC_SYSCLKSource_HSI); } int main(void) { // 初始化系统 SystemClock_Init(); USART1_Init(); // 读取Flash参数 Read_Params(); // 验证参数有效性 if(deviceParams.magic != MAGIC_NUMBER) { // 初始化默认参数 memset(&deviceParams, 0, sizeof(ParamStruct)); deviceParams.magic = MAGIC_NUMBER; // 示例默认数据 uint8_t defaultData[58] = {0x00}; memcpy(deviceParams.data, defaultData, 58); // 写入Flash Write_Params(); } while(1) { // 主循环可添加其他任务 __WFI(); // 进入休眠模式等待中断 } } ``` ### 代码功能说明 1. **参数存储结构**: ```c typedef struct { uint32_t magic; // 魔数验证标识(0x55AA55AA) uint8_t data[58]; // 参数数据区 uint8_t padding[2]; // 填充字节(确保64字节对齐) } ParamStruct; ``` - 使用魔数(MAGIC_NUMBER)验证参数有效性 - 数据区大小58字节匹配用户需求 - 填充字节确保结构体64字节对齐(Flash写入要求) 2. **Flash操作**: - 使用最后一页Flash(0x08007C00)存储参数[^2] - `Read_Params()`:直接拷贝Flash内容到结构体 - `Write_Params()`:先擦除整页再按字(32位)写入 - 操作流程:解锁→擦除→写入→加锁 3. **串口通信**: - USART1配置为115200bps/8N1 - 中断驱动接收:`USART1_IRQHandler()` - 接收缓冲区存储58字节数据包 - 发送单字符状态反馈: - 'O':操作成功 - 'F':格式错误 - 'E':写入错误 4. **数据校验**: ```c uint8_t Validate_DataFormat(uint8_t* data) { // 位置校验 if(data[2] != 0x25 || data[5] != 0x25) return 0; // 结束标志校验 if(data[56] != 0x17 || data[57] != 0x02) return 0; // 校验和验证 uint8_t checksum = 0; for(int i = 0; i < 57; i++) checksum += data[i]; if(checksum != data[57]) return 0; return 1; } ``` - 包含3种校验方式(可根据实际需求修改) - 位置校验:检查特定索引的值 - 结束标志:验证数据包尾部特征 - 校验和:简单和校验机制 ### 使用流程 1. **首次上电**: - 检测Flash无有效参数(魔数不匹配) - 初始化默认参数并写入Flash 2. **数据接收**: ```mermaid sequenceDiagram PC->>STM32: 发送58字节数据包 STM32->>STM32: 接收并存储到缓冲区 STM32->>STM32: 执行格式校验 alt 校验成功 STM32->>STM32: 更新参数结构 STM32->>Flash: 擦除并写入新参数 STM32-->>PC: 发送'O' else 校验失败 STM32-->>PC: 发送'F' end ``` 3. **参数更新**: - 校验成功后立即更新Flash - 系统重启后自动加载新参数 ### 关键注意事项 1. **Flash寿命管理**: - STM32F0 Flash约10,000次擦写寿命 - 避免频繁写入,添加写保护计数器 - 使用EEPROM模拟技术延长寿命[^3] 2. **中断安全**: - Flash操作期间禁用中断 - 使用`__disable_irq()`和`__enable_irq()` - 防止写入过程中断导致数据损坏 3. **数据完整性**: ```c // 三重保护机制 if(deviceParams.magic != MAGIC_NUMBER) // 魔数校验 if(Validate_DataFormat(rxBuffer)) // 格式校验 if(Write_Params() == FLASH_COMPLETE) // 写入校验 ``` > 完整工程需包含标准库文件和启动文件,编译时确保优化等级-O0,避免Flash操作被优化[^1]。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值