STM32单片机开发:控制LED灯仿真实验

实验目标

在51单片机的开发中,我们直接配置51单片机的寄存器,控制芯片的工作方式,配置的时候,我们常常要查阅寄存器表,看用到哪些配置位,然后编写控制寄存器的代码,从而控制单片机的工作。
虽然STM32单片机比51单片机复杂,提供的寄存器数量也非常多,但开发方式和51单片机的开发方式基本相同。本实验将使用直接配置寄存器的方式来编写STM32单片机程序。
在原理图中,我们将两个LED灯接在GPIOB的PB0和PB1端口,要让单片机驱动这两个端口输出高低电平,需要配置PB0和PB1端口的工作模式,并且把这两个端口的时钟打开,让端口能够工作。
《技术参考手册》详细描述了STM32单片机寄存器的配置方式,GPIOB属于外设,重点查阅《技术参考手册》外设使能的寄存器(启动外设时钟的工作称为外设使能)。

配置RCC_APB2ENR寄存器

GPIOx外设都挂载到APB2总线,《技术参考手册》6.3.7节描述了APB2外设时钟使能寄存器的配置方式,6.3.7节给出了寄存器RCC_APB2ENR的配置方式。
RCC_APB2ENR是32位寄存器,它是RCC寄存器组中配置外设的一个寄存器,高16位保留没有使用,默认值为0,低16位用于使能外设(如GPIO、ADC等)时钟,使能GPIOA时钟的控制位是第2个二进制位。
该位的名称为IOPAEN,该位设置为1,使能IO端口A时钟,该位设置为0,则关闭IO端口A时钟。
如何编写C代码来配置RCC寄存器组呢?
STM32单片机的寄存器地址和结构体类型定义,在STM 32官方固件库的stm32f10x.h进行了封装,开发者通过封装的结构体可以直接对相关寄存器进行配置。


typedef struct
{
  __IO uint32_t CR;
  __IO uint32_t CFGR;
  __IO uint32_t CIR;
  __IO uint32_t APB2RSTR;
  __IO uint32_t APB1RSTR;
  __IO uint32_t AHBENR;
  __IO uint32_t APB2ENR;
  __IO uint32_t APB1ENR;
  __IO uint32_t BDCR;
  __IO uint32_t CSR;


} RCC_TypeDef;

RCC_TypeDef结构体对RCC寄存器组进行了封装,该结构体封装了CR、CFGR、……、APB2ENR、……寄存器,每个寄存器占用4个字节的存储空间,APB2ENR在该结构的偏移地址为0x18。对RCC寄存器组的操作通过宏定义RCC进行:

#define RCC   ((RCC_TypeDef *) RCC_BASE)

宏定义RCC将RCC_BASE指向的地址强制转换为RCC_TypeDef结构指针,RCC_BASE实际指向RCC寄存器组在内存的映射地址。

stm32f10x.h文件也给出了RCC_APB2ENR寄存器位的宏定义:
利用前面描述的宏定义,我们可以通过下面的C代码使能GPIO寄存器:
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;

配置GPIO端口的工作模式和输出方式

使能GPIOB外设后,还需要配置GPIOB的工作模式,让GPIOB各端口能够输出高低电平。在《技术参考手册》搜索“GPIO”关键词:
GPIO寄存器配置描述见《技术参考手册》的8.2节,端口配置低寄存器(GPIOx_CRL)用于配置GPIOx的Px0~Px7端口,端口配置高寄存器(GPIOx_CRH)用于配置GPIOx的Px8~Px15端口,因此我们需要配置GPIOA_CRH寄存器。
STM32的一组GPIO有16个IO端口,比如GPIOA组,有GPIOA0~GPIOA15一共16个IO端口。每一个IO端口需要寄存器的4位用来配置工作模式。一组GPIO需要16x4=64位的寄存器来存放这一组GPIO的工作模式的配置,但STM32的寄存器都是32位的,所以只能使用2个32位的寄存器来存储配置信息。GPIOx_CRL用来存放低八位的IO端口(GPIOx0—GPIOx7)配置信息,GPIOx_CRH用来存放高八位IO口(GPIOx8—GPIOx15)配置信息。
MODEy占两个二进制位,用来配置端口是输出模式还是输入模式,y表示GPIOx的第几个端口,如MODE8表示GPIOx的第8个端口,MODE9表示第9个端口。MODEy使用四种组合来配置端口的输入和输出模式:00 输入模式;01:输出模式,最大速度10MHZ;10 输出模式,最大速度2MHZ;11 输出模式,最大速度50MHZ。
本文案例设置MODE8和MODE9的值为01,输出模式,最大速度10MHZ。
CNFy占两个二进制位,用来配置端口的工作模式,CNFy使用四种组合来配置端口的工作模式: 00 通用推挽输出模式;01 通用开漏输出模式;10 复用功能推挽输出模式;11 复用功能开漏输出模式。
本文案例主要使用通用推挽输出模式,该模式可以让GPIO端口输出高低电平,低电平是0V,高电平是3.3V,GPIO端口输出低电平时,与该端口连接的发光二极管点亮,GPIO端口输出高电平时,与该端口连接的发光二极管熄灭。
本文案例设置CNF8和CNF9的值为00,通用推挽输出模式。
如何编写C代码来配置GPIOx寄存器组呢?
typedef struct
{
  __IO uint32_t CRL;
  __IO uint32_t CRH;
  __IO uint32_t IDR;
  __IO uint32_t ODR;
  __IO uint32_t BSRR;
  __IO uint32_t BRR;
  __IO uint32_t LCKR;
} GPIO_TypeDef;

GPIO_TypeDef结构体对GPIOx寄存器组进行了封装,该结构体封装了CRL、CRH、……、ODR、……寄存器,每个寄存器占用4个字节的存储空间,对GPIOx寄存器组的操作通过宏定义GPIOx进行:

宏定义GPIOx将GPIOx_BASE指向的地址强制转换为GPIO_TypeDef结构指针,GPIOx_BASE实际指向GPIOx寄存器组在内存的映射地址。
应用下面的代码可以将GPIOA寄存器组的CRH寄存器低8位清零,并配置GPIOA端口PA8和PA9为通用推挽输出模式,最大工作频率为10MHZ。
GPIOA->CRH &= 0xffffff00;    // 0到7位清零
GPIOA->CRH |= 0x00000011;  // 位0置1,位4置1

控制GPIO端口输出高低电平

GPIOx寄存器组的ODR寄存器用于控制端口输出高低电平。
ODRy占1个二进制位,y表示y表示GPIOx的第几个端口。例如:ODR8是GPIOx的第8个端口,ODR9是GPIOx的第9个端口,ODR8位设置为1,则第8个端口输出高电平,若设置为0,则第8个端口输出低电平。
应用下面的代码可以将GPIOA的PA8设置为高电平:
GPIOA->ODR |= 1<<8;
代码将1左移8位,然后与GPIOA的ODR寄存器进行或操作,相当于把ODR寄存器的第8位设置为1。
应用下面的代码可以将GPIOA的PA8设置为低电平:
GPIOA->ODR &= ~(1<<8);
代码将1左移8位取反,然后与GPIOA的ODR寄存器进行与操作,相当于把ODR寄存器的第8位设置为0。

电路设计

启动proteus8,打开《STM32单片机的电源配置》一节建立的仿真工程,将两个发光二极管引入电路原理图,并连接到单片机PB0、PB1端口。

编写程序

新建工程

启动STM32CubeIDE,新建STM32工程,固件选择STM32F103R6T6A,其它参数无需选择。
输入工程名称,选择工程存储目录,工程默认存储到工作环境目录。其它选项保持默认。
确认STM32工程创建信息,具体包括固件型号、工程存储目录和代码生成选项。代码生成选项保持默认即可。确认后STM32CubeIDE会自动下载固件相应代码,并生成工程相关文件。

工程文件

STM32CubeIDE创建的工程主要由以下几个部分构成:
‌Core文件夹‌:存放程序代码,包括源文件和头文件,是开发者主要编写和修改代码的地方。
‌Drivers文件夹‌:包含与硬件外设相关的驱动程序文件。
‌Debug/Release文件夹‌:根据编译配置生成的文件夹,用于存放编译后的输出文件。

编辑代码

STM32CubeIDE已经为我们创建了基本代码,具体实现代码还需要手动编写。本实验主要编辑工程内main.c文件,驱动单片机的GPIOA的PA8和PA9端口。
main.c文件在工程的Core->src目录下,STM32CubeIDE自动生成的main.c代码调用了固件库的API,固件库是ST公司针对STM32提供的函数接口,开发者可以调用这些函数接口来配置STM32的寄存器,使开发人员得以脱离最底层的寄存器操作,提供STM32单片机的开发效率。
本实验我们依然采用直接配置寄存器的方式来控制STM32单片机,主要是能够直观了解到STM32单片机寄存器的配置方法,也能够沿用51单片机的开发方法。在后续的实验中,我们将逐步由寄存器直接操作方式转变为固件库开发方式,并建立项目工程意识。
包含必要的头文件
#include <stm32f1xx.h>

stm32f1xx.h头文件定义了各种常量,如寄存器地址、位掩码等,用于简化对硬件寄存器的访问和操作。

定义延迟函数
void delay_ms(void)
{
int i,j;
for(i=1000;i>0;i--)
for(j=600;j>0;j--);
}
delay_ms(void)函数提供延时功能,该延迟方法并不是精确的时间延迟方法,它依赖于CPU的时钟速度(即系统时钟频率)。如果系统时钟频率(SYSCLK)发生变化,延时的实际时间也会相应变化。但在本实验中,该方法可以使用。
定义发光二极管初始化函数
void LED_Init(void)
{
RCC->APB2ENR|=1<<3;
GPIOB->CRL&=0X00000000;//清零
GPIOB->CRL|=0X33333333;//50MHz输出模式
GPIOB->ODR=0X00FF;      //输出高
}

LED_Init(void)初始化一个或多个连接到GPIOB端口的LED,代码使用了STM32标准外设库的直接寄存器访问方式。

RCC->APB2ENR|=1<<3;用于使能GPIOB端口的时钟,STM32每个GPIO端口在使用前都需要使能其时钟。RCC->APB2ENR是一个寄存器,用于控制高速APB2总线上外设的时钟使能。1<<3是将数字1左移3位,得到的结果是8(二进制00001000)。这意味着我们要使能的是第3位对应的外设时钟(在APB2总线上,从0开始计数)。对于STM32F10x系列,GPIOB的时钟使能位是RCC_APB2ENR寄存器的第3位。
GPIOB->CRL&=0X00000000;将GPIOB的低配置寄存器(CRL,Configuration Register Low)清零。GPIOB端口有16个引脚,CRL寄存器控制前8个引脚(PB0到PB7)的模式和配置。清零操作是为了确保在配置这些引脚之前,它们的设置是已知的、确定的。
GPIOB->CRL|=0X33333333;将CRL寄存器设置为0x33333333,这个值配置了GPIOB的前8个引脚为通用推挽输出模式,最大速度为50MHz。在STM32中,每个引脚的模式和速度是通过配置寄存器中的两位来设置的。0x33333333中的每个0x33(二进制00110011)对应两个引脚,分别设置它们的模式为推挽输出(01),输出速度为50MHz(11,最高速度)。
GPIOB->ODR=0X00FF;设置GPIOB的输出数据寄存器(ODR,Output Data Register)为0x00FF。ODR寄存器控制GPIOB端口的输出电平。将ODR设置为0x00FF意味着将GPIOB的前8个引脚(PB0到PB7)全部设置为高电平。如果这些引脚连接到了LED(且LED是通过低电平点亮的),那么这行代码实际上会关闭这些LED。如果LED是通过高电平点亮的,那么这行代码会点亮这些LED。
函数代码的目的是初始化GPIOB的前8个引脚为通用推挽输出模式,最大速度为50MHz,并将它们全部设置为高电平,关闭与其引脚连接的LED。这是嵌入式编程中常见的硬件初始化步骤,用于准备外设(在这里是LED)以供后续使用。
main函数
int main(void)
{
int i;
LED_Init();
while(1)
{
for(i=0;i<8;i++)
{
GPIOB->ODR=~(1<<i);
delay_ms();
}
}
}

函数代码通过GPIOB端口的8个引脚(PB0到PB7)控制8个LED(当前只接入了两个LED),实现流水灯效果。

GPIOB->ODR=~(1<<i);用于控制GPIOB端口的输出数据寄存器(ODR)。1<<i将数字1左移i位,得到的结果是一个只有一个位被设置为1的数(这个位对应于要操作的GPIOB端口引脚)。然后,~运算符对这个数进行按位取反操作,得到的结果是一个除了被操作的引脚位为0之外,其他所有位都为1的数。将这个结果赋值给GPIOB->ODR会将除了当前被操作的引脚之外的所有引脚设置为高电平(LED会熄灭),而将当前被操作的引脚设置为低电平(LED会点亮)。

编译和运行程序

配置输出HEX文件
STM32CubeIDE窗口,选择菜单栏Project -> Properties,弹出项目属性对话框。
项目属性对话框左侧列表项,选择C/C++Build->Settings。在项目属性对话框右侧Tool Settings选项卡内,选择“MCU/MPU Post build outputs”列表项,在其右侧的检查项中选中“Convert to Intel Hex fiile”。
构建工程
STM32CubeIDE窗口,选择菜单栏Project -> Build All,STM32CubeIDE会在Console窗口输出构建信息,若构建过程出现问题,根据错误信息进行修改。
运行程序
构建成功的HEX程序存储在项目的Debug目录下,启动Proteus8,打开《STM32单片机的电源配置》一节建立的仿真工程,为STM32F103R6单片机设置运行程序。程序运行效果如下图所示: