STM32/51单片机编程入门(点亮LED)
STM32/51单片机编程入门(点亮LED)
实验任务
- 安装并熟悉Proteus电路仿真软件,完成一个C51程序设计和仿真:实现流水灯。
- 安装MDK5开发环境和STM32包,通过寄存器方式用某一个GPIO端口点亮LED。
实验准备
- Proteus
- Keil5
- STM32最小系统板
- 面包板
- LED灯
- 杜邦线和跳线若干
实验过程
1. C51实现流水灯
- 打开Proteus软件,创建新工程后,选择需要的元器件:51单片机
AT89C51
、发光二极管LED-BIBY
、电阻RES
。
- 将电路连接成如下形式(选择共阳极接法,P2口控制流水灯)。
- 打开Keil4,创建新工程,编写代码。
#include <REGX52.H>
#include <INTRINS.H>
void Delay500ms() //@11.0592MHz
{
unsigned char i, j, k;
_nop_();
i = 4;
j = 129;
k = 119;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
void main() {
while(1) {
P2 = 0xFE; //1111 1110
Delay500ms();
P2 = 0xFD; //1111 1101
Delay500ms();
P2 = 0xFB; //1111 1011
Delay500ms();
P2 = 0xF7; //1111 0111
Delay500ms();
P2 = 0xEF; //1110 1111
Delay500ms();
P2 = 0xDF; //1101 1111
Delay500ms();
P2 = 0xBF; //1011 1111
Delay500ms();
P2 = 0x7F; //0111 1111
Delay500ms();
}
}
- 仿真效果
2. STM32点亮LED(寄存器方式)
-
代码编写
打开Keil5,创建新工程,并创建test.c文件,编写下列代码:
//宏定义,用于存放stm32寄存器映射 #define PERIPH_BASE ((unsigned int)0x40000000)//AHB #define APB2PERIPH_BASE (PERIPH_BASE + 0x10000) #define GPIOA_BASE (APB2PERIPH_BASE + 0x0800) //GPIOA_BASE=0x40000000+0x10000+0x0800=0x40010800,该地址为GPIOA的基地址 #define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00) //GPIOB_BASE=0x40000000+0x10000+0x0C00=0x40010C00,该地址为GPIOB的基地址 #define GPIOC_BASE (APB2PERIPH_BASE + 0x1000) //GPIOC_BASE=0x40000000+0x10000+0x1000=0x40011000,该地址为GPIOC的基地址 #define GPIOD_BASE (APB2PERIPH_BASE + 0x1400) //GPIOD_BASE=0x40000000+0x10000+0x1400=0x40011400,该地址为GPIOD的基地址 #define GPIOE_BASE (APB2PERIPH_BASE + 0x1800) //GPIOE_BASE=0x40000000+0x10000+0x0800=0x40011800,该地址为GPIOE的基地址 #define GPIOF_BASE (APB2PERIPH_BASE + 0x1C00) //GPIOF_BASE=0x40000000+0x10000+0x0800=0x40011C00,该地址为GPIOF的基地址 #define GPIOG_BASE (APB2PERIPH_BASE + 0x2000) //GPIOG_BASE=0x40000000+0x10000+0x0800=0x40012000,该地址为GPIOG的基地址 #define GPIOA_ODR_Addr (GPIOA_BASE+12) //0x4001080C #define GPIOB_ODR_Addr (GPIOB_BASE+12) //0x40010C0C #define GPIOC_ODR_Addr (GPIOC_BASE+12) //0x4001100C #define GPIOD_ODR_Addr (GPIOD_BASE+12) //0x4001140C #define GPIOE_ODR_Addr (GPIOE_BASE+12) //0x4001180C #define GPIOF_ODR_Addr (GPIOF_BASE+12) //0x40011A0C #define GPIOG_ODR_Addr (GPIOG_BASE+12) //0x40011E0C #define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2)) #define MEM_ADDR(addr) *((volatile unsigned long *)(addr)) #define LED0 MEM_ADDR(BITBAND(GPIOA_ODR_Addr,8)) //#define LED0 *((volatile unsigned long *)(0x422101a0)) //PA8 //定义typedef类型别名 typedef struct { volatile unsigned int CR; volatile unsigned int CFGR; volatile unsigned int CIR; volatile unsigned int APB2RSTR; volatile unsigned int APB1RSTR; volatile unsigned int AHBENR; volatile unsigned int APB2ENR; volatile unsigned int APB1ENR; volatile unsigned int BDCR; volatile unsigned int CSR; } RCC_TypeDef; #define RCC ((RCC_TypeDef *)0x40021000) //定义typedef类型别名 typedef struct { volatile unsigned int CRL; volatile unsigned int CRH; volatile unsigned int IDR; volatile unsigned int ODR; volatile unsigned int BSRR; volatile unsigned int BRR; volatile unsigned int LCKR; } GPIO_TypeDef; //GPIOA指向地址GPIOA_BASE,GPIOA_BASE地址存放的数据类型为GPIO_TypeDef #define GPIOA ((GPIO_TypeDef *)GPIOA_BASE) void LEDInit(void) { RCC->APB2ENR|=1<<2; //GPIOA 时钟开启 GPIOA->CRH&=0XFFFFFFF0; GPIOA->CRH|=0X00000003; } //粗略延时 void Delay_ms(volatile unsigned int t) { unsigned int i,n; for (n=0;n<t;n++) for (i=0;i<800;i++); } int main(void) { LEDInit(); while (1) { LED0=0;//LED熄灭 Delay_ms(500);//延时时间 LED0=1;//LED亮 Delay_ms(500);//延时时间 } }
-
电路连接
这里使用面包板和STM32最小系统板进行电路连接。其中LED的正极与PA8引脚连接,负极接地。
-
实际效果图
将代码烧入到STM32中,编译运行,可以看到如下效果。
思考题
-
(1) 嵌入式C程序代码对内存(RAM)中的各变量的修改操作,与对外部设备(寄存器--->对应相关管脚)的操作有哪些相同与差别?
答:
相同点:
- 数据访问方式: 无论是内存中的变量还是外部设备的寄存器,它们都可以通过C程序中的变量来访问。可以使用变量名来读取和写入数据。
- 数据类型: 在C中,可以使用相同的数据类型(例如int、char、float等)来定义内存中的变量和表示寄存器中的数据。这意味着可以使用相同的数据类型来处理它们。
- 赋值操作: 无论是内存中的变量还是寄存器中的值,都可以通过赋值操作进行修改。可以使用赋值运算符(=)来将新值分配给变量或寄存器。
差异:
- 物理位置: 内存中的变量存储在RAM(随机存储器)中,而外部设备的寄存器通常与处理器的I/O端口相连,用于与外部设备进行通信。因此,内存和寄存器的物理位置不同。
- 访问速度: 通常情况下,内存的访问速度较慢,而寄存器的访问速度较快。因此,对寄存器的操作可能比对内存的操作更快。
- 特定硬件操作: 对寄存器的操作通常需要特定的硬件寄存器映射,而对内存中的变量的操作是标准的C变量操作。因此,与内存中的变量不同,对寄存器的操作可能需要使用特殊的寄存器地址和指令。
- 中断和外部事件: 外部设备通常通过中断或外部事件来触发操作,而内存中的变量不会具有这种行为。因此,对外部设备的操作可能需要与中断处理程序或外部事件处理相关联。
- 硬件限制: 外部设备的寄存器操作受到硬件限制和规范的约束,而内存中的变量受到更少的限制。在对外部设备进行操作时,需要遵循相关的硬件规范和文档。
(2) 为什么51单片机的LED点灯编程要比STM32的简单?
答:
-
架构简单: 51单片机是基于较早的8位架构,拥有相对简单的指令集和寄存器结构。这种简单性使得编写程序相对容易,特别是对于初学者来说。
-
资源有限: 51单片机通常具有有限的存储器(ROM和RAM)和少量的I/O引脚,因此只需要处理少量的寄存器和少量的内存,这使得编程工作相对简单。
-
直接寻址: 51单片机的内存和I/O寄存器通常可以通过直接寻址访问,这意味着你可以直接读写特定的内存地址或寄存器,而不需要复杂的外设库或驱动程序。
-
较低的复杂性: STM32系列等高性能嵌入式处理器通常配备了丰富的外设和复杂的架构,这为处理更复杂的任务提供了更多的灵活性,但也增加了编程的复杂性。相比之下,51单片机的应用通常相对简单,例如LED点灯。
-
嵌入式C程序经常会看见 register和volatile 关键字,请解释这两个变量修饰符的作用,并用C代码示例进行说明。
答:
- register
在嵌入式C语言程序中,register
关键字用于建议编译器将某个变量存储在CPU寄存器中,而不是存储在内存中。这可以减少内存访问的开销,以提高变量的访问速度。
#include <stdio.h>
int main() {
// 定义一个普通的整数变量
int a = 10;
// 定义一个带有 register 关键字的整数变量
register int b = 20;
// 访问并修改变量 a
a = a + 5;
// 访问并修改变量 b
b = b + 5;
// 打印变量 a 和 b 的值
printf("a = %d\n", a);
printf("b = %d\n", b);
return 0;
}
在上述代码中,我们定义了两个整数变量 a
和 b
。a
是普通的整数变量,而 b
带有 register
关键字。接下来,我们对这两个变量分别执行加法操作,然后打印它们的值。
使用 register
关键字可以向编译器发出信号,告诉它某个变量在代码中频繁使用,建议将其存储在寄存器中,以提高访问速度。不过,我们不能对 register
变量执行取地址操作,因为寄存器没有内存地址。
- volatile
volatile
关键字在嵌入式C语言中的作用是告诉编译器不要对标记为 volatile
的变量进行优化或缓存。它的主要用途是告诉编译器,这个变量的值可能会在程序的控制之外被改变,因此编译器不应该对其进行任何优化,以确保在需要时可以正确访问该变量的最新值。
#include <stdio.h>
int main() {
// 声明一个 volatile 变量
volatile int sensorValue = 0;
// 模拟传感器读取操作,该值可能会在程序控制之外更改
sensorValue = 42;
// 使用 volatile 变量
printf("Sensor Value: %d\n", sensorValue);
return 0;
}
在上述代码中,sensorValue
被标记为 volatile
,因为它表示一个传感器的值,该值可能会在程序的控制之外由传感器硬件更改。因此,编译器不会对对 sensorValue
的读取进行优化,以确保在需要时可以正确读取传感器的最新值。
总结&体会
通过本次实验,我了解了Keil和Proteus等软件的基本使用,并且能够分别使用C51和STM32来实现点灯,也对51单片机和STM32的区别有了一定的认识。其中比较大的困难,就是理解STM32代码中的大量的寄存器映射和类型定义。