熟悉单片机的朋友都知道,学习的第一个例程就是流水灯,要想实现流水灯,首先必须了解GPIO的工作原理。GPIO的基本结构如图1所示。
STM32 的 IO 口可以由软件配置成如下 8 种模式:
STM32 有很多的内置外设,这些外设的外部引脚都是与 GPIO 复用的。也就是说,一个 GPIO如果可以复用为内置外设的功能引脚,那么当这个 GPIO 作为内置外设使用的时候,就叫做复用。当I/O端口被配置为复用功能时:
●在开漏或推挽式配置中,输出缓冲器被打开
●内置外设的信号驱动输出缓冲器(复用功能输出)
●施密特触发输入被激活
●弱上拉和下拉电阻被禁止
●在每个APB2时钟周期,出现在I/O脚上的数据被采样到输入数据寄存器
●开漏模式时,读输入数据寄存器时可得到I/O口状态
●在推挽模式时,读输出数据寄存器时可得到最后一次写的值
大家都知道, MCU 都有串口, STM32 有好几个串口。比如说 STM32F103ZET6 有 5 个串口,我们可以查手册知道,串口 1 的引脚对应的 IO 为 PA9,PA10.PA9, PA10 默认功能是 GPIO,所以当PA9,PA10 引脚作为串口 1 的 TX,RX 引脚使用的时候,那就是端口复用。
USART1_TX | PA9 |
---|---|
USART1_RX | PA10 |
复用端口初始化有几个步骤:
1) GPIO 端口时钟使能。要使用到端口复用,当然要使能端口的时钟了。RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
2)复用的外设时钟使能。比如你要将端口 PA9,PA10 复用为串口,所以要使能串口时钟。RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
3)端口模式配置。
关于串口的内容后面的章节会详细讲解。
为了使不同器件封装的外设 IO 功能数量达到最优,可以把一些复用功能重新映射到其他一些引脚上。 STM32 中有很多内置外设的输入输出引脚都具有重映射(remap)的功能。我们知道每个内置外设都有若干个输入输出引脚,一般这些引脚的输出端口都是固定不变的,为了让设计工程师可以更好地安排引脚的走向和功能,在 STM32 中引入了外设引脚重映射的概念,即一个外设的引脚除了具有默认的端口外,还可以通过设置重映射寄存器的方式,把这个外设的引脚映射到其它的端口。
复用功能 | USART1_REMAP = 0 | USART1_REMAP = 1 |
---|---|---|
USART1_TX | PA9 | PB6 |
USART1_RX | PA10 | PB7 |
从表中可以看出,默认情况下,串口 1 复用的时候的引脚位 PA9、PA10,同时我们可以将 TX 和 RX 重新映射到管脚 PB6 和 PB7 上面去。所以重映射我们同样要使能复用功能的时候讲解的 2 个时钟外,还要使能 AFIO 功能时钟,然后要调用重映射函数。
详细步骤为:
1)使能 GPIOB 时钟:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
2)使能串口 1 时钟:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
3)使能 AFIO 时钟:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
4)开启重映射:
GPIO_PinRemapConfig(GPIO_Remap_USART1,ENABLE);
这样就将串口的 TX 和 RX 重映射到管脚 PB6 和 PB7 上面了。
发光二极管是属于二极管的一种,具有二级管单向导电特性,即只有在正向电压(二极管的正极接正,负极接负)下才能导通发光。PB0引脚接发光二极管(LED1)的正极,所以PB0引脚输出高电平LED1亮,PB0引脚输出低电平LED1熄灭,,LED2,LED3同理。
值得注意的,不同的开发板,LED连接的GPIO一般是不同的,请注意修改。
要想真正掌握一款单片机,分析寄存器是必不可少,但是对于STM32来再说,ST已经将寄存器操作封装成库函数,开发者只需要调用库函数即可,对于初学者来说,只需学会使用使用函数即可,笔者在《入门篇》也说了为何要来使用库函数以及使用库开发的好处,因此对于没有基础的读者朋友就不必细究每个寄存器,当学到一定程度,再来一探究竟吧,笔者再这里只是给出GPIO的寄存配置相关配置表,在后面的章节也是如此。好了,继续进入正题吧。
每个GPIO端口都有两个32位配置寄存器(GPIOx_CRL ,GPIOx_CRH) ,两个32位数据寄存器 (GPIOx_IDR和GPIOx_ODR),一个32位置位/ 复位寄存器(GPIOx_BSRR),一个16位复位寄存器(GPIOx_BRR),一个32位锁定寄存器(GPIOx_LCKR)。每个I/O端口位可以自由编程,然而I/O端口寄存器必须按32位字被访问(不允许半字或字节访问) 。
点亮LED,基本步骤是:配置寄存器;控制寄存器。库开发只是将传统的配置方式编程函数,是的单片机开发变得简单方便快捷。
我们常用的 IO 端口寄存器只有 4 个: CRL、 CRH、 IDR、 ODR。其中CRL 和 CRH 控制着每个 IO 口的模式及输出速率。
低配置寄存器 CRL 的描述,如下图和下表所示。
CRL和CRH类似,在此就不在赘述了,读者朋友可以参看《STM32F10XXX参考手册》数据输入输出寄存器是将对应的IO口置位,从而进行数据的输入与输出。
笔者在上文已经分析了GPIO的原理及操作步骤,现在我们就来写代码吧。本书是用库来对STM32来开发的,这是本书的第一个实例,笔者为了读者比较直接配置寄存和库开发的区别,笔者在此用了两种方式进行开发,希望读者能自己体会两种方式的优劣。这里笔者会使用两种库:标准库和HAL库。
不管使用何种实现方式,其流程都是一样的。
GPIO是开发STM32最基本的配置,所以掌握GPIO的配置显得尤为重要。要实现流水灯,一般步骤可以总结为如下:
先看主函数。
/*Includes*********************************************************************/
#include"./LED/stm32f103_led.h"
/*延时函数*/
void Delay(u32 xms);
/**
* @brief 主函数
* @param None
* @retval int
*/
int main(void)
{
/*LED 初始化 */
LED_GPIO_Config();
while(1)
{
//ODR GPIOB
//15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
//0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 (复位值)
//1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0
GPIOB->ODR= 0XFFFE; //低电平,GPIOB0(LED1)灯灭
Delay(0x0FFFFF);
GPIOB->ODR= 0XFFFF; //高电平,GPIOB0(LED1)灯亮
//ODR GPIOG
//15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
//0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 (复位值)
//1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1
GPIOG->ODR= 0XFFBF; //低电平,GPIOG6(LED2)灯灭
Delay(0x0FFFFF);
GPIOG->ODR= 0XFFFF; //高电平,GPIOG6(LED2)灯亮
//ODR GPIOG
//15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
//0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 (复位值)
//1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1
GPIOG->ODR= 0XFF7F; //低电平,GPIOG7(LED3)灯灭
Delay(0x0FFFFF);
GPIOG->ODR= 0XFFFF; //高电平,GPIOG7(LED3)灯亮
}
}
/**
* @brief 延时函数
* @param
xms 延时长度
* @retval None
*/
void Delay( u32 xms)
{
//for(;nCount != 0; nCount--);(方法一)
while(xms--);//(方法二)
}
首先是GPIO初始化,然后就是无限循环,不断设置GPIO的高低电平。
GPIO初始化代码如下:
/**
* @brief 初始化LED的GPIO
* @param None
* @retval None
*/
void LED_GPIO_Config(void)
{
/*开启 GPIO 时钟,使用外设时都要先开启它的时钟*/
RCC->APB2ENR|= 1 << 3; //使能PORTB时钟
RCC->APB2ENR|= 1 <<8 ; //使能PORTG时钟
/* 配置GPIO为通用推挽输出模式,速率为50M */
GPIOB->CRL&= 0XFFFFFFF0;
GPIOB->CRL|= 0X00000003; //PB.0 推挽输出
GPIOB->ODR|= 1 << 0; //PB.0 输出高
GPIOG->CRL&= 0X00FFFFFF;
GPIOG->CRL|= 0X33000000; //PG.6, PG.7推挽输出
GPIOG->ODR|= 3 << 6; //PG.6, PG.7输出高
}
控制LED的GPIO主要做了以下事情:
1.开启GPIO时钟
2.设置GPIO的输出模式和速率,具体寄存器配置请参看CRL寄存器。
3.最后设置GPIO为高电平(非必须)。
最后在主函数不断设置GPIO的高低电平即可实现流水灯。
GPIO库函数相关的库函数如下:
核心代码如下:
/*Includes*********************************************************************/
#include"./LED/stm32f103_led.h"
/*简单延时函数*/
void Delay(u32 xms);
/**
* @brief 主函数
* @param None
* @retval
*/
int main(void)
{
/*LED 初始化 */
LED_GPIO_Config();
while(1)
{
LED1(ON ); // 亮
Delay(0xfFfff);
LED1(OFF ); // 灭
LED2(ON ); // 亮
Delay(0xfFfff);
LED2(OFF ); // 灭
LED3(ON ); // 亮
Delay(0xffFff);
LED3(OFF ); // 灭
}
}
/**
* @brief 延时函数
* @param
xms 延时长度
* @retval None
*/
void Delay( u32 xms)
{
//for(;nCount != 0; nCount--);(方法一)
while(xms--);//(方法二)
}
使用库函数的流程和寄存器版的一样,唯一不同的是这里使用了库函数,而库函数也是对寄存器的封装与抽象。
关于如何使用STM32Cube新建工程在前文已经讲解过了,这里直说配置GPIO部分内容。本文要实现流水灯,其实输出为初始化设置为高电平还是低电平都可以,因为流水灯需要不断反转。
1.首先进行系统配置,debug模式选择可以选择其他模式,默认选择No debug,Timebase Source选择SysTick,关于Timebase Source在后文会详细讲解。
2.时钟配置,外部高速时钟。
配置系统时钟配置72MHz,APB1总线配置36MHz。默认高速时钟是使用内部(HSI),而且CPU时钟配置的比较低。以我选择的STM32F103,外部8M晶振为例(如下图)。
3.GPIO初始化配置
我们将PB0、PG6、PG7配置输出模式(高电平、低电平均可)、输出速率、上/下拉等,默认即可。
4.工程生成设置
其他默认即可。最后点击“生成代码”即可生成代码。
我们要实现流水灯,需要在主函数添加一下代码。
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_0);
HAL_Delay(500);
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_0);
HAL_GPIO_TogglePin(GPIOG,GPIO_PIN_6);
HAL_Delay(500);
HAL_GPIO_TogglePin(GPIOG,GPIO_PIN_6);
HAL_GPIO_TogglePin(GPIOG,GPIO_PIN_7);
HAL_Delay(500);
HAL_GPIO_TogglePin(GPIOG,GPIO_PIN_7);
在分析代码前,先看看流水灯编程流程:
1>使能GPIO端口时钟;
2>初始化GPIO引脚,即为GPIO初始化结构体赋值,并调用相应初始化函数完成初始化配置;
3>根据实际需求控制流水灯。
STM32采用固件库开发,代码比较多,因此,我们在看一个实际工程时,只需从主函数开始,好了,接下来,笔者就带领大家一步一步看看流水灯是怎么实现的。笔者先贴出主函数代码。
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCUConfiguration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and theSystick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
/* USER CODE BEGIN 2 */
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
HAL_Delay(500);
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
HAL_GPIO_TogglePin(GPIOG, GPIO_PIN_6);
HAL_Delay(500);
HAL_GPIO_TogglePin(GPIOG, GPIO_PIN_6);
HAL_GPIO_TogglePin(GPIOG, GPIO_PIN_7);
HAL_Delay(500);
HAL_GPIO_TogglePin(GPIOG, GPIO_PIN_7);
}
/* USER CODE END 3 */
}
注释部分不用管,我们只看代码。
1. 复位所有硬件
HAL_Init()函数为复位所有硬件,这个就不细说了。
HAL_StatusTypeDef HAL_Init(void)
{
/* Configure Flash prefetch */
#if (PREFETCH_ENABLE != 0)
#if defined(STM32F101x6) ||defined(STM32F101xB) || defined(STM32F101xE) || defined(STM32F101xG) || \\
defined(STM32F102x6) ||defined(STM32F102xB) || \\
defined(STM32F103x6) ||defined(STM32F103xB) || defined(STM32F103xE) || defined(STM32F103xG) || \\
defined(STM32F105xC) ||defined(STM32F107xC)
/* Prefetch buffer is not available on value line devices */
__HAL_FLASH_PREFETCH_BUFFER_ENABLE();
#endif
#endif /* PREFETCH_ENABLE */
/* Set Interrupt Group Priority */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
/* Use systick as time base source and configure 1ms tick (default clockafter Reset is HSI) */
HAL_InitTick(TICK_INT_PRIORITY);
/* Init the low level hardware */
HAL_MspInit();
/* Return function status */
return HAL_OK;
}
2. 时钟初始化函数
SystemClock_Config()函数用于时钟初始化,也就是通过STM32Cube实现。
时钟初始化函数如下:
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Initializes the RCC Oscillators according to the specifiedparameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) !=HAL_OK)
{
Error_Handler();
}
}
RCC_OscInitTypeDef结构体类型定义时钟来源和系统时钟生成配置,就是图9中的配置,本例程使用外部8MHz晶振,通过PLL锁相环9倍频后得到72M系统时钟。
RCC_ClkInitTypeDef结构体类型定义总线时钟配置,一般选择使能系统时钟、AHB、APB1和APB2总线时钟,其中只有APB1总线时钟36MHz,其他都为72MHz。
HAL_RCC_ClockConfig函数就是HAL定义的一个系统滴答定时器初始化配置函数,通过这个函数得到延时效果。
3. GPIO初始化函数
MX_GPIO_Init()函数用于LED的GPIO初始化,代码如下:
static void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOG_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOG, GPIO_PIN_6|GPIO_PIN_7, GPIO_PIN_RESET);
/*Configure GPIO pin : PB0 */
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/*Configure GPIO pins : PG6 PG7 */
GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOG, &GPIO_InitStruct);
}
以上就是LED的初始化函数,初始化GPIO有个重要的结构体GPIO_InitTypeDef,先看看该结构体。
typedef struct
{
uint32_t Pin; /*!
在初始化代码中,还有一个重要的函数HAL_GPIO_WritePin()。HAL_GPIO_WritePin()函数为3个LED灯时钟初始化状态,这里设置为低电平,所以初始化状态3个LED都是暗的。
4. 流水灯实现
在前文已经贴出了流水灯的相应代码。
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_0);
HAL_Delay(500);
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_0);
HAL_GPIO_TogglePin(GPIOG,GPIO_PIN_6);
HAL_Delay(500);
HAL_GPIO_TogglePin(GPIOG,GPIO_PIN_6);
HAL_GPIO_TogglePin(GPIOG,GPIO_PIN_7);
HAL_Delay(500);
HAL_GPIO_TogglePin(GPIOG,GPIO_PIN_7);
}
HAL_GPIO_TogglePin函数用于GPIO电平反转,HAL_Delay用于延时,单位是毫秒,以此不断将LED1、LED2、LED3关灭,这就是实现流水灯的效果。
将编译好的程序下载到板子中,可以看到三个LED灯不同地闪烁。
本章虽然用不同的方式实现了流水灯,也就是不断控制GPIO的高低电平,我想大家会有很多疑问,寄存器、表混库、HAL的区别是什么,流水灯是怎么一步步调用等等,后面的章节会一一解答,尽请期待!
小贴士:上拉电阻
1.当 TTL 电路驱动 CMOS 电路时,如果 TTL 电路输出的高电平低于 CMOS电路的最低高电平(一般为 3.5V),这时就需要在 TTL 的输出端接上拉电阻,以提高输出高电平的值。
2.OC(集电极开路)门电路必须加上拉电阻,才能使用。
3.为加大输出引脚的驱动能力,有的单片机管脚上也常使用上拉电阻。
4.在 CMOS 芯片上,为了防止静电造成损坏,不用的管脚不能悬空,一般接上拉电阻产生降低输入阻抗,提供泄荷通路。
5.芯片的管脚加上拉电阻来提高输出电平,从而提高芯片输入信号的噪声容限增强抗干扰能力。
6.提高总线的抗电磁干扰能力。管脚悬空就比较容易接受外界的电磁干扰。
7.长线传输中电阻不匹配容易引起反射波干扰,加上下拉电阻是电阻匹配,有效的抑制反射波干扰。
以上三点,通常在 1k 到 10k 之间选取。对下拉电阻也有类似道理对上拉电阻和下拉电阻的选择应结合开关管特性和下级电路的输入特性进行设定,主要需要考虑以下几个因素:
全部0条评论
快来发表一下你的评论吧 !