[读书日志]8051软核处理器设计实战(基于FPGA)第三篇:8051 keil编程配置 C语言开发流程 中断程序实例

ops/2025/1/7 13:39:11/

第一篇icon-default.png?t=O83Ahttps://blog.csdn.net/m0_74021449/article/details/144796689

第二篇icon-default.png?t=O83Ahttps://blog.csdn.net/m0_74021449/article/details/144813103

3.8051中断与keil开发流程

3.1 keil的下载与概述

关于keil,大家都并不陌生,它是开发51单片机和ARM架构的32单片机的有力工具。关于下载安装方法这里不多赘述,请参阅其他文章。需要注意的是:keil分为ARM和C51版本,我们需要安装C51的版本。

下载完keil后我们可以在安装路径找到这些文件:

点击进入C51文件夹,找到Examples文件夹,点击进入:  

其中包含很多文件夹,这些文件夹是一些示例程序,有针对特定芯片的工程,也有不针对特定芯片的通用工程。我们点击进入Hello文件夹,来看看最简单的工程:

双击hello.uvproj即可打开keil的工程,界面如下:  

为了方便说明,把这段程序复制如下:

/*------------------------------------------------------------------------------
HELLO.C
​
Copyright 1995-2005 Keil Software, Inc.
------------------------------------------------------------------------------*/
​
#include <REG52.H>                /* special function register declarations   *//* for the intended 8051 derivative         */
​
#include <stdio.h>                /* prototype declarations for I/O functions */
​
​
#ifdef MONITOR51                         /* Debugging with Monitor-51 needs   */
char code reserve [3] _at_ 0x23;         /* space for serial interrupt if     */
#endif                                   /* Stop Exection with Serial Intr.   *//* is enabled                        */
​
​
/*------------------------------------------------
The main C function.  Program execution starts
here after stack initialization.
------------------------------------------------*/
void main (void) {
​
/*------------------------------------------------
Setup the serial port for 1200 baud at 16MHz.
------------------------------------------------*/
#ifndef MONITOR51SCON  = 0x50;               /* SCON: mode 1, 8-bit UART, enable rcvr      */TMOD |= 0x20;               /* TMOD: timer 1, mode 2, 8-bit reload        */TH1   = 221;                /* TH1:  reload value for 1200 baud @ 16MHz   */TR1   = 1;                  /* TR1:  timer 1 run                          */TI    = 1;                  /* TI:   set TI to send first char of UART    */
#endif
​
/*------------------------------------------------
Note that an embedded program never exits (because
there is no operating system to return to).  It
must loop and execute forever.
------------------------------------------------*/while (1) {P1 ^= 0x01;                 /* Toggle P1.0 each time we print */printf ("Hello World\n");   /* Print "Hello World" */}
}

这段代码的含义是把“Hello World\n”这12个字符(包含空格和换行符)从初始化完毕后的串口按照顺序写入SBUF寄存器上,之后单片机通过串口把SBUF内的字符通过串口输出。当我们打开串口调试助手,可以看到单片机发回的字符。由于写入while(1)循环,将会不断输出这句话。在电脑上使用printf时,字符被打印到屏幕上,但对于单片机而言,这些字符需要一个指定的打印终端,在这里我们指定串口为打印终端。在打印之前,程序先对串口进行了配置,以保证其正常输出。

#ifndef MONITOR51SCON  = 0x50;               /* SCON: mode 1, 8-bit UART, enable rcvr      */TMOD |= 0x20;               /* TMOD: timer 1, mode 2, 8-bit reload        */TH1   = 221;                /* TH1:  reload value for 1200 baud @ 16MHz   */TR1   = 1;                  /* TR1:  timer 1 run                          */TI    = 1;                  /* TI:   set TI to send first char of UART    */
#endif

这段程序就是配置串口的过程,其中SCON、TMOD、TH1、TR1、TI都是8051的寄存器,这些对于我们使用8051MCU进行编程很重要,但对于我们自己设计的软核处理器不必要过多关注,在这里不多赘述,详细内容可以参阅51单片机神级教程——江协科技视频。

这些寄存器的地址在REG52.H这个头文件中存放,但直接打开工程无法找到这个头文件,点击编译(左上角两个向下箭头的rebuild键),这个文件将被自动编译并添加。

/*  BYTE Registers  */
sfr P0    = 0x80;
sfr P1    = 0x90;
sfr P2    = 0xA0;
sfr P3    = 0xB0;
sfr PSW   = 0xD0;
sfr ACC   = 0xE0;
sfr B     = 0xF0;
sfr SP    = 0x81;
sfr DPL   = 0x82;
sfr DPH   = 0x83;
sfr PCON  = 0x87;
sfr TCON  = 0x88;
sfr TMOD  = 0x89;
sfr TL0   = 0x8A;
sfr TL1   = 0x8B;
sfr TH0   = 0x8C;
sfr TH1   = 0x8D;
sfr IE    = 0xA8;
sfr IP    = 0xB8;
sfr SCON  = 0x98;
sfr SBUF  = 0x99;

此处只列举了一小部分,详细内容大家可以自己去查看。这里使用sfr关键字定义,在 C 语言中,sfr 关键字通常用于定义一个 8 位特殊功能寄存器的别名。sfr指定特定的寄存器,sbit指定寄存器的特定位:

/*  BIT Registers  */
/*  PSW  */
sbit CY    = PSW^7;
sbit AC    = PSW^6;
sbit F0    = PSW^5;
sbit RS1   = PSW^4;
sbit RS0   = PSW^3;
sbit OV    = PSW^2;
sbit P     = PSW^0; //8052 only

这里定义的就是PSW寄存器各个位的名称和地址,我们使用定义的名称可以直接访问寄存器特定的位。

3.2 keil的配置

我们回顾一下,单片机是如何运行起来的。我们在keil中使用C语言编写程序,keil将其转换为汇编代码和机器指令,并生成对应二进制文件,这个二进制文件放入我们设计的8051软核的代码区,便可使其执行对应程序。那么我们需要首先配置keil输出二进制文件。

 

选中项目根目录:Simulator,点击魔术棒,弹出配置窗口。

点击到device页,可以看到目前默认的是8XC52,它拥有2个DPTR,256Byte RAM,说明它是一款8052型单片机。8051的片内RAM是128Byte,8052是256Byte。接下来回到target页面:

在Memory Model界面下拉有3个选项,表示将变量存储在DATA、PDATA或XDATA中。

  • DATA:前面所述的DATA区,大小128字节,使用MOV指令0x00~0x7F可以直接访问;

  • PDATA:XDATA的低256字节,地址空间0x00~0xFF,使用MOVX@Rn指令访问(Rn寄存器间接寻址),可以直接使用8bit地址访问;

  • XDATA:片外的XDATA存储区,使用MOVX指令访问,需要提供16bit地址(MOVX@DPTR),大小为64KB。

Memory Model设置的是变量存储位置,下一个选项Code Rom Size则设置的是程序大小,三个选项的含义如下:

  • Small:整个工程不超过2KB;

  • Compact:每个子函数不超过2KB,整个工程最大可达64KB;

  • Large:整个工程可以是64KB,子函数大小无限制。

配置完成后,我们编写的C语言程序就有了运行空间,程序编写完成,经过keil编译,就可以得到ROM空间的二进制程序,把这个二进制数据送入真正单片机的ROM中,就可以执行程序了。为了获得二进制文件,我们还需要进行一系列配置:

首先在刚才的设置窗口点到Output页面,勾选Create Hex File选项,这样编译器会在编译完成后输出Hex文件:

然而我们实际使用的时候,一般不适用hex文件,而是需要使用纯二进制文件(bin),我们需要下载一个插件“HEX2BIN.exe”,将其放入安装目录下:(\Keil_v5\C51\BIN)  

然后在keil中刚才的设置界面的User页面,选中After Build/Rebuild的第一项,输入以下代码:

输入的代码是这个:

$k\C51\BIN\hex2bin.exe -s 0 -p 0 @H.hex

点击OK,回到主界面,点击Rebuild,会出现如下提示:

 

打开工程目录,发现已经生成bin文件,说明配置成功。

 

3.3 keil工程的创建

了解了大概流程后,我们新建一个工程,点击Project->New uVersion Project,之后选择文件夹,设置文件名,来到这个界面:

在这里可以选择对应单片机型号,由于我们要自己设计一个软核处理器,所以我们在这里选择Generic,这里可以选择4种型号,我们选择8051:

点击OK,会弹出一个窗口,询问是否选择汇编程序STARTUP.A51作为起始程序,我们这里不需要,点击否,可进入工程主页面。

点击File->new新建一个文件,输入以下代码:

void main(void) {
​
}

ctrl+s将其另存为.c文件,之后右键点击Source Group1,将其添加到工程中。点击Rebuild,输出如下信息:

 

这里的code=16说明这段代码最后合成了16字节的指令组合,我们查看其具体内容,在菜单中选择Debug->Start/stop Debug Session,进入调试界面:

左侧这部分是之前学过的8051的寄存器,上面是对应的代码序列,下面是我们的程序,这里我们详细逐句解读代码:

C:0x0000    020003   LJMP     C:0003
C:0x0003    787F     MOV      R0,#0x7F
C:0x0005    E4       CLR      A
C:0x0006    F6       MOV      @R0,A
C:0x0007    D8FD     DJNZ     R0,C:0006
C:0x0009    758107   MOV      SP(0x81),#0x07
C:0x000C    02000F   LJMP     main(C:000F)
C:0x000F    22       RET  

   

首先来看第一条指令,我们先从形式上解析一下。前面的“C:0x0000”代表这条指令的地址,“020003”是以十六进制表示的机器码,我们将其转换为16进制:“0000 0010_0000 0000_0000 0011”可见其是3字节的。我们回顾LJMP的指令组成:

{{0000_0010},{address[15:8]},{address[7:0]}}这里的address是十六bit的十六进制数0003,我们发现,这个机器码就是我们前面学习的指令组成,第一字节是指令识别序列,在这里第二三字节组合在一起是跳转地址。

后面的LJMP C:0003就是和机器码一一对应的注记汇编指令。

所以我们明白了,keil生成的一条指令是这样构成的:

C:0x0000    020003    LJMP     C:0003
指令地址     机器码     汇编代码

那么它生成的二进制bin文件又是怎么存储的呢?我们使用winhex打开它生成的bin文件:(注意:当你每一次创建新的工程,都需要把刚才的配置步骤重新完成,主要是设置输出Hex文件和在User界面输入代码以生成Bin文件;还有在hex输出界面,新建工程默认会创建一个Object文件夹,把hex输出放在那个文件夹中,这会导致hex2bin程序找不到这个文件,必须重新设置,选择输出路径和当前项目的目录一致,就是uproj的同一个目录下)

可见02 00 03这就是我们第一条指令的机器码,后面也可完全对应。说明bin文件中写入的就是生成指令序列的机器码。16字节也恰好是机器码的长度。

研究完它的输出,我们继续解析这些指令。刚才已经讲解了第一条指令,它的含义是跳转到0003地址继续执行,那么这个地址其实也就是我们第二条指令的地址。

C:0x0003    787F     MOV      R0,#0x7F  ;设置R0值为0x7F(DATA区最高地址)
C:0x0005    E4       CLR      A         ;将ACC值清0
C:0x0006    F6       MOV      @R0,A     ;将A写入R0保存的地址对应数据区,即将0写入0x7F
C:0x0007    D8FD     DJNZ     R0,C:0006 ;循环,R0减一,不等于0就跳转,意为清楚DATA区所有数据

这4条指令执行的作用是让DATA区清0,具体的含义见上面的注释。说明:DATA区的地址是00H~7FH,这里先将最高位地址(7FH)存入R0,使用寄存器间接寻址,清楚这个地址的数据,之后再进行循环,使R0减一,不断执行操作,直到最后地址变为00H。这段内容可对应第二章反复对照学习。

C:0x0009    758107   MOV      SP(0x81),#0x07
C:0x000C    02000F   LJMP     main(C:000F)
C:0x000F    22       RET 

接着,设置了堆栈段初始值,SP是栈顶地址,这表明除了寄存器组的第一组(R0~R7),剩下的DATA区都会用作堆栈区。之后的LJMP作用是跳转到主程序进行执行,由于main函数中没有任何语句,所以直接返回(RET),但作为主函数,它不能返回,因此我们应该在刚才的程序中加入一条死循环,放置程序跳转回不可控地区。

void main(void) {while(1);
}

这时我们再重新编译,调试,看看它的汇编代码:

C:0x0000    020003   LJMP     C:0003
C:0x0003    787F     MOV      R0,#0x7F
C:0x0005    E4       CLR      A
C:0x0006    F6       MOV      @R0,A
C:0x0007    D8FD     DJNZ     R0,C:0006
C:0x0009    758107   MOV      SP(0x81),#0x07
C:0x000C    02000F   LJMP     main(C:000F)2:         while(1); 
C:0x000F    80FE     SJMP     main(C:000F)

发现最后的RET返回指令没有了,取而代之的是一条SJMP指令,其跳转回自己的地址,即反复执行这句话,达到了死循环的效果。

3.4 一个C语言程序实例

现在我们了解到keil是如何把C语言转换为CPU可以执行的指令序列。无论多么复杂的程序,本质上都是这些基本指令序列的组合。使用C语言开发单片机,和我们在电脑上使用C语言有一些不同之处,最大的区别是我们经常需要指定一个特定的寄存器。

假设我们编写这样一段代码:

sfr DISPLAY = 0xc0;
​
void print(char *str) {while(*str!='\0'){DISPLAY = *str;str++;}
}
​
void main (void) {print("Hello World\n");while(1);
}

其中,DISPLAY是我们自定义的接收打印字符的寄存器,“sfr DISPLAY = 0xc0”,表示它位于SFR区的C0位置,我们前面已经说明过,SFR区中空余的位置我们可以定义自己的寄存器。但是现在这个函数无法打印变量,因为它不是系统函数printf。

下面这个程序解决了这个问题:

#include<stdio.h>
void main (void) {char i = 0;while(1){printf("Hello World %d \n",i);i++;}
}

这个系统函数调用的是‘’C51/LIB‘的“PUTCHAR.c”函数来打印,如下:

char putchar (char c)  {
​if (c == '\n')  {if (RI)  {if (SBUF == XOFF)  {do  {RI = 0;while (!RI);}while (SBUF != XON);RI = 0; }}while (!TI);TI = 0;SBUF = 0x0d;                         /* output CR  */}if (RI)  {if (SBUF == XOFF)  {do  {RI = 0;while (!RI);}while (SBUF != XON);RI = 0; }}while (!TI);TI = 0;return (SBUF = c);
}

这个函数使用系统定义的串口接收寄存器SBUF,如果想使用自定义的寄存器可以重写putchar函数:

#include<stdio.h>
​
sfr DISPLAY = 0xc0;
​
char putchar(char c) {return (DISPLAY = c);
}
​
void main (void) {char i = 0;while(1){printf("Hello World %d \n",i);i++;}
}

3.5 8051中断与中断程序编写

中断是一种“意外”,可能是程序员故意添加的,也可能是由于内部执行错误自动抛出的。我们在这里讨论的是第一种,即我们自己创造的中断。如果没有中断,程序会按照既定的序列不断执行,但有时我们需要人为干预它的执行,当我们给出这个执行中断信号时,处理器应当保存当前指令的下一条指令地址,然后跳转到中断向量处开始执行,然后在执行完中断程序后再次回到之前的程序。

8051最多支持32个中断,这些中断指向的地址都是以0x3或0xB结尾的。这32个中断向量分别是0x03、0x0B、0x13、0x1B......0xF3、0x0B。处理一条中断,相当于处理器执行了一次LCALL指令,但这是强迫发生的,而非程序自然执行发生的。

中断控制器应当具备接收和发出的功能,即接收RETI指令,这条指令除了使程序回到之前的位置继续执行,还通知中断控制器之前的中断执行结束,中断控制器只有接收到这个信号后才能再次发起中断。中断控制器发出的是一个模拟LCALL指令,引导程序从当前执行位置跳转到中断向量开始执行。

在上一段程序中,我们编写中断向量程序:

#include<stdio.h>
​
sfr DISPLAY = 0xc0;
​
char putchar(char c) {return (DISPLAY = c);
}
​
void inter(void) interrupt 1 {printf("There is 1 interrupt\n");
}
​
void main (void) {char i = 0;while(1){printf("Hello World %d \n",i);i++;}
}

void inter(void) interrupt 1是中断服务程序的开头。interrupt关键字后面是中断序号,范围是0-31,这里interrupt 1代表中断跳转地址是0x0B。

我们重新编译后,点击调试,将会在汇编程序中找到这条语句:

C:0x000B    0203F8   LJMP     inter(C:03F8)

这条长跳转语句放在中断向量处,引导跳转到inter函数,这是因为两个中断向量之间只有8字节位置,不够存放长的程序。

     9: void inter(void) interrupt 1 { 
C:0x03F8    C0E0     PUSH     ACC(0xE0)
C:0x03FA    C0F0     PUSH     B(0xF0)
C:0x03FC    C083     PUSH     DPH(0x83)
C:0x03FE    C082     PUSH     DPL(0x82)
C:0x0400    C0D0     PUSH     PSW(0xD0)
C:0x0402    75D000   MOV      PSW(0xD0),#0x00
C:0x0405    C000     PUSH     0x00
C:0x0407    C001     PUSH     0x01
C:0x0409    C002     PUSH     0x02
C:0x040B    C003     PUSH     0x03
C:0x040D    C004     PUSH     0x04
C:0x040F    C005     PUSH     0x05
C:0x0411    C006     PUSH     0x06
C:0x0413    C007     PUSH     0x0710:     printf("There is 1 interrupt\n"); 
C:0x0415    7BFF     MOV      R3,#0xFF
C:0x0417    7A04     MOV      R2,#0x04
C:0x0419    7939     MOV      R1,#0x39
C:0x041B    120070   LCALL    PRINTF(C:0070)11: } 12:  
C:0x041E    D007     POP      0x07
C:0x0420    D006     POP      0x06
C:0x0422    D005     POP      0x05
C:0x0424    D004     POP      0x04
C:0x0426    D003     POP      0x03
C:0x0428    D002     POP      0x02
C:0x042A    D001     POP      0x01
C:0x042C    D000     POP      0x00
C:0x042E    D0D0     POP      PSW(0xD0)
C:0x0430    D082     POP      DPL(0x82)
C:0x0432    D083     POP      DPH(0x83)
C:0x0434    D0F0     POP      B(0xF0)
C:0x0436    D0E0     POP      ACC(0xE0)
C:0x0438    32       RETI    

可以看到,从0x03F8开始执行中断程序,执行中断程序需要保存现场和恢复现场,我们看到程序将ACC、B、DPH、DPL、PSW、R0~R7全部保存到堆栈中,之后又全部取出。其中保存完PSW后,重新设置了PSW,这是因为之前程序调用的是寄存器组0,而中断程序也要调用寄存器组0,所以需要先保存之前的数据,然后再设置调用寄存器组0,最后再将其复原。如果我们希望让其更加简洁,可以指定中断程序使用寄存器组1,这样它们就不会发生冲突。

#include<stdio.h>
​
sfr DISPLAY = 0xc0;
​
char putchar(char c) {return (DISPLAY = c);
}
​
void inter(void) interrupt 1 using 1{printf("There is 1 interrupt\n");
}
​
void main (void) {char i = 0;while(1){printf("Hello World %d \n",i);i++;}
}

这次我们再检查中断程序会发现其更加简洁:

     9: void inter(void) interrupt 1 using 1{ 
C:0x041F    C0E0     PUSH     ACC(0xE0)
C:0x0421    C0F0     PUSH     B(0xF0)
C:0x0423    C083     PUSH     DPH(0x83)
C:0x0425    C082     PUSH     DPL(0x82)
C:0x0427    C0D0     PUSH     PSW(0xD0)
C:0x0429    75D008   MOV      PSW(0xD0),#0x0810:     printf("There is 1 interrupt\n"); 
C:0x042C    7BFF     MOV      R3,#0xFF
C:0x042E    7A03     MOV      R2,#0x03
C:0x0430    79F8     MOV      R1,#0xF8
C:0x0432    120070   LCALL    PRINTF(C:0070)11: } 12:  
C:0x0435    D0D0     POP      PSW(0xD0)
C:0x0437    D082     POP      DPL(0x82)
C:0x0439    D083     POP      DPH(0x83)
C:0x043B    D0F0     POP      B(0xF0)
C:0x043D    D0E0     POP      ACC(0xE0)
C:0x043F    32       RETI    

对于市面上MCU的51单片机编程,我们需要仔细阅读它的手册,学习配置寄存器。而对于我们自己设计的软核处理器便可以随意很多,我们可以依据我们的需要创造寄存器,比如设置一个协处理器,将其配置寄存器放在SFR区。本质上来说,处理器只负责按照C语言的调度访问寄存器,再写入寄存器,仅仅是这样简单功能的叠加便可成为我们多彩的程序。使用FPGA设计时,我们可以使其物尽其用,需要什么就添加什么,不需要的可以大刀阔斧地改进。


http://www.ppmy.cn/ops/147819.html

相关文章

enzymejest TDD与BDD开发实战

一、前端自动化测试需要测什么 1. 函数的执行逻辑&#xff0c;对于给定的输入&#xff0c;输出是否符合预期。 2. 用户行为的响应逻辑。 - 对于单元测试而言&#xff0c;测试粒度较细&#xff0c;需要测试内部状态的变更与相应函数是否成功被调用。 - 对于集成测试而言&a…

大数据-266 实时数仓 - Canal 对接 Kafka 客户端测试

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; Java篇开始了&#xff01; MyBatis 更新完毕目前开始更新 Spring&#xff0c;一起深入浅出&#xff01; 目前已经更新到了&#xff1a; H…

STM32G070CB的USART1_RX引脚

简介 在使用STM32G070CBT6 的 USART1时&#xff0c;发现把 PA10作为 USART1_RX引脚时&#xff0c;接收不到数据。 问题排查 更换pin脚 使用PB6/PB7作为USART1_TX/RX&#xff0c; USART1 工作正常。 使用PA9/PB7作为USART1_TX/RX&#xff0c; USART1 同样工作正常。 示波器…

tensorflow 内存错误

使用tensorflow训练多个模型时&#xff0c;训练过程中容易出现内存错误&#xff0c;在这里记录一下解决办法。希望能帮到各位。 2025-01-02 22:31:03.489713: W tensorflow/core/common_runtime/bfc_allocator.cc:275] Allocator (GPU_0_bfc) ran out of memory trying to all…

无刷直流电机(BLDC)六步换向法

文章目录 1、三相BLDCM 基本结构2、三相BLDCM 数学模型3、有霍尔位置传感器直流无刷电机工作原理4、无位置传感器直流无刷电机工作原理5、速度检测6、六步换向双闭环模型仿真6.1 模型总览6.2 系统及参数设置6.3 六步换向模块6.4 仿真效果 7、六步换向速度闭环PWM控制参考 1、三…

数据库高安全—角色权限:角色创建角色管理

目录 3.1 角色创建 3.2 角色管理 书接上文openGauss安全整体架构&安全认证&#xff0c;从安全整体架构与安全认证两方面&#xff0c;对高斯数据库的高安全性能进行了解读&#xff0c;本篇我们将从角色创建和角色管理两方面对高斯数据库的角色权限进行介绍。 3.1 角色创建…

Golang设计模式目录

go语言实现设计模式 1 文章目录&#xff1a; 1.1 创建型模式 1.Golang设计模式之工厂模式2.Golang设计模式之抽象工厂模式3.Golang设计模式之单例模式4.Golang设计模式之建造者模式5.Golang设计模式之原型模式 1.2 结构型模式 6.Golang设计模式之适配器模式7.Golang设计模式之桥…

浅谈棋牌游戏开发流程二:后端技术选型与基础环境搭建

一、前言&#xff1a;客户端只是台前&#xff0c;后端才是幕后“指挥中心” 在上一篇“客户端技术”中&#xff0c;我们聊到玩家看到的一切动作、动画、界面逻辑&#xff0c;都靠客户端去渲染和交互。但若没有后端的支撑&#xff0c;玩家点了“出牌”可能就像一拳打在空气里—…