menu 牢记自己是菜
STM32开发学习----week1
1442 浏览 | 2021-05-20 | 阅读时间: 约 7 分钟 | 分类: 计算机 | 标签:
请注意,本文编写于 1071 天前,最后修改于 1071 天前,其中某些信息可能已经过时。

0x1 前言

今年主要节奏还是考研,晚上课余时间稍微的玩一下板子。看了有两小节,实验感觉和微机原理接口实验没太大区别,可能比较大的区别是微机原理接口实验主体研究的是各类数字芯片,而STM32是一个开发板吧。STM32软件编程使用的是C51,看起来还有点生疏,汇编启动代码有点像LC3(也可能不是后续在认真看一下)。我使用的是正点原子mini版,没有其他外设,后续如果需要的话在进行加购。之后更新频率保证一周一更,只作为个人纪录。

学习方向(暂定):

  1. 环境搭建
  2. 复现基本例程,包括跑马灯在内的所有实验
  3. 尝试自己开发一些小程序,自己烧一下板子
  4. 尝试在自己开发的小程序下完成一些基础的安全实验(好像要求有点高,iot安全??)

0x2 环境搭建

具体的环境安装不多赘述,我使用的Keil uVision5进行工程的构建,使用FLYMCU对程序进行烧写,XCOM进行串口输出。

注意:工程构建分为库函数工程和寄存器工程,注意区分构建工程过程。

0x3 GPIO工作原理

GPIO(英语:General-purpose input/output),通用型之输入输出的简称,功能类似8051的P0—P3,其接脚可以供使用者由程控自由使用,PIN脚依现实考量可作为通用输入(GPI)或通用输出(GPO)或通用输入与输出(GPIO),如当clk generator, chip select等。

工程目录结构

首先我们看一下工程目录

我们的代码主要是写在main中的,对于其余的结构我们有如下层次:

从层次图中可以看出,我们的用户代码和 HARDWARE 下面的外设驱动代码再不需要直接 操作寄存器,而是直接或间接操作官方提供的固件库函数。 相当于FWLib为我们提供了封装的寄存器方法,可以直接调用,为我们抽象了寄存器。

STM32 的每个 IO 端口都有 7 个寄存器来控制。他们分别是:配置模式的 2 个 32 位的端口 配置寄存器 CRL 和 CRH;

配置寄存器主要负责控制IO口的运行方式,类比微机原理接口中的芯片设置寄存器,至于如何为STM32写入数据现在还不太清楚后续稍微注意一下!!(由于STM32开发主要使用的是C51而不是直接的X86汇编,所以很可能在FWLib中是有现成的封装函数的)。

来了来了了!! 在固件库开发中,操作寄存器 CRH 和 CRL 来配置 IO 口的模式和速度是通过 GPIO 初始化 函数完成的,这里详细的解释一下这几个关键函数(一般初始化的函数是写在HARDWARE下的):

IO函数

初始化函数

void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);

相关结构体:

{ uint16_t GPIO_Pin; 
 GPIOSpeed_TypeDef GPIO_Speed; 
 GPIOMode_TypeDef GPIO_Mode; 
}GPIO_InitTypeDef;
/*##################################################*/
typedef enum
{ GPIO_Mode_AIN = 0x0, //模拟输入
 GPIO_Mode_IN_FLOATING = 0x04, //浮空输入
 GPIO_Mode_IPD = 0x28, //下拉输入
 GPIO_Mode_IPU = 0x48, //上拉输入
 GPIO_Mode_Out_OD = 0x14, //开漏输出
 GPIO_Mode_Out_PP = 0x10, //通用推挽输出
 GPIO_Mode_AF_OD = 0x1C, //复用开漏输出
 GPIO_Mode_AF_PP = 0x18 //复用推挽
}GPIOMode_TypeDef;
/*##################################################*/
typedef enum
{ 
 GPIO_Speed_10MHz = 1,
 GPIO_Speed_2MHz, 
 GPIO_Speed_50MHz
}GPIOSpeed_TypeDef;

初始化例程:

GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //LED0-->PB.5 端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//速度 50MHz
GPIO_Init(GPIOB, &GPIO_InitStructure);//根据设定参数配置 GPIO

这里解释一下这里的初始化例程,首先第一个参数是用来指定 GPIO,取值范围为 GPIOA~GPIOG,也就是我们想要设置的IO。

第二个参数其实是一个设置的结构体,里面包含这三个成员,分别是用来设置端口配置,输出模式与输出速率。

初始化一个GPIO_InitTypeDef结构体,为其成员函数赋值再进行调用即可。

IO读取函数

uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)

IO输入函数

void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);

STM32功能表与相关寄存器

具体STM32功能表:

至于这些模式是个什么鬼东西,后续再说。 STM32 输出模式配置 :

2 个 32 位的数据寄存器 IDR 和 ODR;

1 个 32 位的置位/复位寄存器 BSRR;

一个 16 位的复位寄存器 BRR;

1 个 32 位的锁存寄存器 LCKR。

这里稍微的学习一下寄存器的大致信息:

STM32的寄存器是32位的,每一个IO口需要4位的控制位,所以一个CRL ( CRH)可以对8个IO口进行配置.对于STM32一组IO口是16位(PA0~PA15),所以一共要使用两个32位寄存器进行配置。CRL用作低8位,CRH用作高8位。

0x4 LED实验库函数版本

在编写函数之前我们要首先的初始化外设,初始化外设的代码可以放在 HARDWARE->LED 文件夹下面 。

LED.c

初始化代码:

void LED_Init(void)
{
 
 GPIO_InitTypeDef  GPIO_InitStructure;
     
 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOD, ENABLE);     //使能PA,PD端口时钟
    
 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;                 //LED0-->PA.8 端口配置
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;          //推挽输出
 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;         //IO口速度为50MHz
 GPIO_Init(GPIOA, &GPIO_InitStructure);                     //根据设定参数初始化GPIOA.8
 GPIO_SetBits(GPIOA,GPIO_Pin_8);                         //PA.8 输出高

 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;                 //LED1-->PD.2 端口配置, 推挽输出
 GPIO_Init(GPIOD, &GPIO_InitStructure);                       //推挽输出 ,IO口速度为50MHz
 GPIO_SetBits(GPIOD,GPIO_Pin_2);                          //PD.2 输出高 
}

该代码里面就包含了一个函数 void LED_Init(void),该函数的功能就是用来实现配置 PA8 和 PD2 为推挽输出。首先我们来看一下硬件原理图:

我们用到了两个输出的IO口,分别是PD2和PA8,连接两个LED灯口。我们直接初始化了一个结构体GPIO_InitTypeDef,设置相关参数,使用之前说到的GPIO_Init直接进行端口设置。

GPIO_SetBits(GPIOA,GPIO_Pin_8);

这里是默认输出电平为高电平。(作用不太清楚,等之后观察一下再说),这里我们就算直接写了一个LED初始的函数,等会可以直接在main函数中进行调用。

嗯,懂了!这里我们的STM32和我们微机原理接口中的芯片是有区别的,在微机原理接口里面,我们要通过控制电位的高低电平才可以完成对LED进行变换,而这里直接对这一个操作进行了封装,我们只需要通过调用函数就可以直接完成对两个输出端口的控制,就好像直接按开关一样,简化了操作,可以让开发人员集更简洁的进行上层软件开发。

GPIO_SetBits(GPIOA, GPIO_Pin_8); //设置 GPIOA8 输出 1,等同 LED0=1;
GPIO_ResetBits (GPIOA, GPIO_Pin_8); //设置 GPIOA8 输出 0,等同 LED0=0;

这里多说一句,我们想要使用LED0=1这样的直接控制,需要引入宏定义,简单来讲,我们需要在初始化的时候将端口与名称进行绑定。

#define LED0 PAout(8) // PA8
#define LED1 PDout(2) // PD2

main.c

之后我们来研究main函数:

#include "led.h"   //这个头文件主要就是绑定LED与IO端口的宏定义头文件
#include "delay.h" //这里封装了延迟函数
#include "sys.h"   //这里主要封装了一些系统函数
 int main(void)
 {    
    delay_init();             //延时函数初始化      
    LED_Init();              //初始化与LED连接的硬件接口,我们刚写的Led.c
    while(1)       
    {
        LED0=0;
        LED1=1;            
        delay_ms(300);     //延时300ms
        LED0=1;
        LED1=0;
        delay_ms(300);    //延时300ms
    }
 }

虽然函数已经为我们封装好了,但是作为学习,我们还是要看一下两个头文件中到底为我们提供了哪些可以使用的函数,和整个实现的方法。

首先我们看一下delay.h文件下的一些函数源码。

//delay_init();       延时函数初始化
void delay_init()
{
#if SYSTEM_SUPPORT_OS                              //如果需要支持OS.
    u32 reload;
#endif
    SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);    //选择外部时钟  HCLK/8
    fac_us=SystemCoreClock/8000000;                //为系统时钟的1/8  
#if SYSTEM_SUPPORT_OS                              //如果需要支持OS.
    reload=SystemCoreClock/8000000;                //每秒钟的计数次数 单位为M  
    reload*=1000000/delay_ostickspersec;        //根据delay_ostickspersec设定溢出时间
                                                //reload为24位寄存器,最大值:16777216,在72M下,约合1.86s左右    
    fac_ms=1000/delay_ostickspersec;            //代表OS可以延时的最少单位       

    SysTick->CTRL|=SysTick_CTRL_TICKINT_Msk;       //开启SYSTICK中断
    SysTick->LOAD=reload;                         //每1/delay_ostickspersec秒中断一次    
    SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk;       //开启SYSTICK    

#else
    fac_ms=(u16)fac_us*1000;                    //非OS下,代表每个ms需要的systick时钟数   
#endif
}                                    

之后是我们使用到的延迟函数,??为啥有两个延迟函数原型,并且没有被重载??:

void delay_ms(u16 nms)
{    
    if(delay_osrunning&&delay_osintnesting==0)    //如果OS已经在跑了,并且不是在中断里面(中断里面不能任务调度)        
    {         
        if(nms>=fac_ms)                            //延时的时间大于OS的最少时间周期 
        { 
               delay_ostimedly(nms/fac_ms);        //OS延时
        }
        nms%=fac_ms;                            //OS已经无法提供这么小的延时了,采用普通方式延时    
    }
    delay_us((u32)(nms*1000));                    //普通方式延时  
}

void delay_ms(u16 nms)
{                     
    u32 temp;           
    SysTick->LOAD=(u32)nms*fac_ms;                //时间加载(SysTick->LOAD为24bit)
    SysTick->VAL =0x00;                            //清空计数器
    SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ;    //开始倒数  
    do
    {
        temp=SysTick->CTRL;
    }while((temp&0x01)&&!(temp&(1<<16)));        //等待时间到达   
    SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;    //关闭计数器
    SysTick->VAL =0X00;                           //清空计数器              
} 

我们稍微的将程序改一改,烧录看一下效果。

后续记一点遇到的问题:

  1. keil默认使用的是C89编译规则,直接启用C99mode即可。
  2. LED.c初始化的函数中GPIO_SetBits(GPIOA,GPIO_Pin_8);对LED灯本身无影响,只是设置了在没有信号的时候初始状态。
  3. 硬件电路问题,仔细看了一下板子的原理图,LED0与LED1是直接在版内被焊丝的,原理图上明确提到,如果将PA8与PD2作为IO的话,两个LED也会收到控制。

0x5 按键输入实验

上一个实验主要是为了掌握IO口的输入,这个实验我们来学习IO口的输出。一个语言掌握了输入和输出,就可以算是小入门了吧。

本小节实验 ,我们将通过MiniSTM32开发板上载有的 3 个按钮(KEY0/KEY1/WK_UP),来控 制板上的 2 个 LED,其中 KEY0 控制 DS0,按一次亮,再按一次,就灭。KEY1 控制 DS1,效 果同 KEY0。WK_UP 按键则同时控制 DS0 和 DS1,按一次,他们的状态就翻转一次。

注意观察一下原理图: KEY0 和 KEY1 是低电平有效的,而 WK_UP 是高电平有效的(根据原理图,WK_UP接的是vc3.3,而剩下两个按键是直接接地的)。除了KEY1有上拉电阻(与 JTDI 共用),其他两个都没有上下拉电阻,所以,需要在 STM32 内部设置上下拉。

我们直接开始软件代码的阅读。

key.c

#include "key.h"
#include "delay.h"
//////////////////////////////////////////////////////////////////////////////////     
//本程序只供学习使用,未经作者许可,不得用于其它任何用途
//ALIENTEK Mini STM32开发板
//按键输入 驱动代码           
//正点原子@ALIENTEK
//技术论坛:www.openedv.com
//修改日期:2014/3/06
//版本:V1.0
//版权所有,盗版必究。
//Copyright(C) 广州市星翼电子科技有限公司 2009-2019
//All rights reserved                                       
//////////////////////////////////////////////////////////////////////////////////     
         
//按键初始化函数 
//PA15和PC5 设置成输入
void KEY_Init(void)
{
    
    GPIO_InitTypeDef GPIO_InitStructure;

     RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOC,ENABLE);//使能PORTA,PORTC时钟

    GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);//关闭jtag,使能SWD,可以用SWD模式调试
    
    GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_15;//PA15
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //设置成上拉输入
     GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA15
    
    GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_5;//PC5
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //设置成上拉输入
     GPIO_Init(GPIOC, &GPIO_InitStructure);//初始化GPIOC5
 
    GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_0;//PA0
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //PA0设置成输入,默认下拉      
    GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.0
    
} 
//按键处理函数
//返回按键值
//mode:0,不支持连续按;1,支持连续按;
//返回值:
//0,没有任何按键按下
//KEY0_PRES,KEY0按下
//KEY1_PRES,KEY1按下
//WKUP_PRES,WK_UP按下 
//注意此函数有响应优先级,KEY0>KEY1>WK_UP!!
u8 KEY_Scan(u8 mode)
{     
    static u8 key_up=1;//按键按松开标志
    if(mode)key_up=1;  //支持连按          
    if(key_up&&(KEY0==0||KEY1==0||WK_UP==1))
    {
        delay_ms(10);//去抖动 
        key_up=0;
        if(KEY0==0)return KEY0_PRES;
        else if(KEY1==0)return KEY1_PRES;
        else if(WK_UP==1)return WKUP_PRES; 
    }else if(KEY0==1&&KEY1==1&&WK_UP==0)key_up=1;          
    return 0;// 无按键按下
}

在key.c中定义了两个函数,一个是KEY_Init,一个是KEY_Scan。KEY_Init是按键的初始化函数,我们来看一下他到底干了什么。

我们用到了3个IO口,所以我们需要设置3个IO口。根据原理图,我们可以看见三个按钮分别接入了PA0,PC5和PA15。使用GPIO_InitTypeDef进行初始化。

    GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_15;//PA15
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //设置成上拉输入
     GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA15

相比较上一个实验我们少设置了GPIO_Speed等参数,留空即可。

这里解释一下上拉输入,和下拉输入,这里其实就是设置按钮到底是摁下去有效还是弹上来有效。比如PA15对应的按钮是KEY_1,而KEY_1在被按下时会产生一个低电平,所以我们要使用上拉输入。简单来讲,接地的开关要使用上拉输出,接电瓶的需要下拉输出。

KEY_Scan则是输入函数,这个实验操作有点类似微机原理接口中的键盘输入实验的操作。目的是检测按键是否有效。这里就不多赘述了。

main.c

#include "led.h"
#include "delay.h"
#include "sys.h"
#include "key.h"
//ALIENTEK Mini STM32开发板范例代码2
//按键输入实验           
//技术支持:www.openedv.com
//广州市星翼电子科技有限公司
 int main(void)
 {    
    u8 t=0;      
    delay_init();             //延时函数初始化      
    LED_Init();                   //初始化与LED连接的硬件接口
    KEY_Init();              //初始化与按键连接的硬件接口
    LED0=0;                    //点亮LED
    while(1)
    {
        t=KEY_Scan(0);        //得到键值
        switch(t)
        {                 
            case KEY0_PRES:
                LED0=!LED0;
                break;
            case KEY1_PRES:
                LED1=!LED1;
                break;
            case WKUP_PRES:                
                LED0=!LED0;
                LED1=!LED1;
                break;
            default:
                delay_ms(10);    
        } 
    }         
}

发表评论

email
web

全部评论 (暂无评论)

info 还没有任何评论,你来说两句呐!