学习STM32单片机编程入门指南

文章目录

  • STM32/51单片机编程入门
  • (一)使用C51实现“HELLO”的数码管
  • 1.安装protues
  • 2.安装Keil
  • 3.使用C51程序设计和仿真,实现数码管输出“HELLO”
  • (二)通过寄存器方式点亮LED灯
  • 1.新建工程
  • 2.LED闪烁
  • (三)STM32系列芯片的原理
  • 1.STM32系列芯片的地址映射和寄存器映射原理
  • 2.嵌入式C程序代码对内存(RAM)中的各变量的修改操作,与对外部设备(寄存器—>对应相关管脚)的操作有哪些相同与差别?
  • 3.为什么51单片机的LED点灯变成要比STM32的简单?
  • (四)C程序中变量修饰符的作用
  • STM32/51单片机编程入门

    (一)使用C51实现“HELLO”的数码管

    1.安装protues

    具体方法:Proteus软件的安装与使用方法(超详细) – 知乎 (zhihu.com)

    2.安装Keil

    Keil5和Keil MDK的区别:Keil5可以进行C51的编程,MDK可以进行stm32的编程,二者可以融合,具体方法见:使keil5同时支持STM32与C51_keil5可以用于stm32吗_快乐的别走的博客-CSDN博客

    3.使用C51程序设计和仿真,实现数码管输出“HELLO”

    3.1数码管

    数码管分为共阴级和共阳极,其中共阳极给低电平有效,共阴级为高电平有效。

    在protues中的区分方式:单独引脚在下边为阴极,单独引脚在上边为阳极。
    3.2仿真图:

    该数码管为动态显示,由于人体的视觉暂留现象,Delay(10)人体无法察觉图像的变化,所以看上去是静态的,若Delay(500)则可以清楚看到数码管的变化。

    3.3代码:

    8段位选码:0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80

    “HELLO”段选码:0x76,0x79,0x38,0x38,0x3F

    #include <REGX51.H>
    
    unsigned char str[]={0x76,0x79,0x38,0x38,0x3F};
    unsigned char wei[]={0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80};
    void delay(unsigned int n)
    {
    	unsigned int j,i;
    	for(i=0;i<n;i++)
    	{
    		for(j=0;j<120;j++);
    	}
    }
    void main()
    {
    	unsigned int i;
    	while(1){
    	for(i=0;i<5;i++)
    	{
    		P3=~wei[i];
    		P2=str[i];
    		delay(10);
    	}
    	}
    }
    
    

    3.4仿真改进

    引入74LS138译码器可以减少IO口的使用

    #include <REGX51.H>
    
    unsigned char str[]={0x76,0x79,0x38,0x38,0x3F};
    unsigned char wei[]={0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07};
    void delay(unsigned int n)
    {
    	unsigned int j,i;
    	for(i=0;i<n;i++)
    	{
    		for(j=0;j<120;j++);
    	}
    }
    void main()
    {
    	unsigned int i;
    	while(1){
    	for(i=0;i<5;i++)
    	{
    		P3=wei[i];
    		P2=str[i];
            delay(10);
    	}
    	}
    }
    
    

    (二)通过寄存器方式点亮LED灯

    1.新建工程

    在菜单栏点击Project中的New uVision Project,在想要的存储地址新建文件夹名为“点亮LED”

    进入该文件夹将项目命名为project并保存,之后选择我们的单片机–STM32F103RB(在我们的STM32最小板的芯片上写了相应的型号),之后点击OK,

    勾选CMSIS下的COFE和Device 下的Startup

    在菜单栏点击File–New,将其保存命名为mian.c(注意:这里命名的名字后面一定要加“.c”)

    在右边的工具栏中找到Source Goupe 1,右击,点击Add Existing Files to Group ‘Source Goupe 1’

    在出现的窗口中选择我们刚刚新建的.c文件,并Add

    如下图为添加成功

    输入以下代码:

    //用来存放STM寄存器映射
    #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);//延时
         }
    }
    
    

    项目没有错误:

    2.LED闪烁

    2.1下载ST-LINK资料包

    http://openedv.com/posts/list/0/62552.htm

    在资料包里,点击dpinst_amd64.exe即可下载

    2.2硬件连接

    点击Keil工具箱的魔术棒,点击Debug里的ST-Link Debuggeer

    点击旁边的Settings,勾选ST-LINK/V2和SW

    之后点击确定,进行编译,编译成功后点击LOAD即可看到LED闪烁


    LED等闪烁:

    (三)STM32系列芯片的原理

    1.STM32系列芯片的地址映射和寄存器映射原理

    地址映射原理:为了保证CPU执行指令时可正确访问存储单元,需将用户程序中的逻辑地址转换为运行时由机器直接寻址的物理地址,这一过程称为地址映射。

    存储器映射:下表列出了STM32F10xxx中内置外设的起始地址,如下表:

    存储器和总线架构:

    Cortex™-M3存储器映像包括两个位段(bit-band)区。这两个位段区将别名存储器区中的每个字

    映射到位段存储器区的一个位,在别名存储区写入一个字具有对位段区的目标位执行读-改-写操

    作的相同效果。

    在STM32F10xxx里,外设寄存器和SRAM都被映射到一个位段区里,这允许执行单一的位段的

    写和读操作。

    下面的映射公式给出了别名区中的每个字是如何对应位带区的相应位的:

    bit_word_addr = bit_band_base + (byte_offset×32) + (bit_number×4)

    其中:

    bit_word_addr是别名存储器区中字的地址,它映射到某个目标位。

    bit_band_base是别名区的起始地址。

    byte_offset是包含目标位的字节在位段里的序号

    bit_number是目标位所在位置(0-31)

    2.嵌入式C程序代码对内存(RAM)中的各变量的修改操作,与对外部设备(寄存器—>对应相关管脚)的操作有哪些相同与差别?

    相同:

    (1)存储位置:内存中的变量和外部设备都需要使用特定的存储位置。变量在内存中有自己的地址,而外部设备通常通过特定的寄存器与处理器或控制器连接,并且每个寄存器都有一个地址。
    (2)数据传输:在代码中,变量的修改或外部设备的操作都需要进行数据传输。无论是将值存储到变量中还是将数据发送到外部设备的寄存器中,都需要进行数据传输。

    不同:

    (1)访问权限:对于内存中的变量,C程序可以直接读取和修改其值,除非变量被声明为常量或指针限定符限制了访问权限。而对于外部设备,其寄存器的访问权限可能受到硬件保护机制的限制,需要通过特定的接口和指令来访问。
    (2)时间延迟:对内存中的变量的修改是即时的,读取和写入的延迟很小,因为内存通常是与处理器直接连接的。而对外部设备的操作通常涉及与设备之间的通信,可能需要等待设备响应,因此有时会出现较大的时间延迟。
    (3)编程接口:对于内存中的变量,可以通过直接访问变量的地址来读取和修改其值。而对于外部设备,通常需要使用特定的接口和寄存器操作来与设备进行通信和控制。这些操作可能需要使用特定的寄存器配置、位操作或特殊指令来实现正确的设备控制。

    总之,尽管在代码层面上,对内存中的变量和外部设备的操作可能存在一些相似之处,但在底层实现和使用接口上存在明显的差异。合理理解和处理这些差异对于编写嵌入式系统中的操作是很重要的。

    3.为什么51单片机的LED点灯变成要比STM32的简单?

    (1)编译语言:

    因为STM32比51的外设、时钟等高出许多,51单片机结构相对简单,所以通常多使用汇编语言和C语言编程,而STM32系列的开发工作,不会采用汇编语言,因为工程量巨大,寄存器太多,位数也多

    (2)编程方式:
    51单片机只需要配置寄存器打开就可以编程,而STM32系列单片机需要先打开对应的时钟,包括开启后打开外部时钟才开始工作

    (3)资源不同:

    STM32的内部资源(寄存器和外设功能)较普通的51单片机都要多,所以在编程上有更多的选择

    (四)C程序中变量修饰符的作用

    1.与PC平台上的一般程序不同,嵌入式C程序经常会看见 register和volatile 关键字,请解释这两个变量修饰符的作用,并用C代码示例进行说明。

    在嵌入式C程序中,经常会看到register和volatile这两个变量修饰符,它们用于对变量的行为和存储进行修饰。

    (1)register关键字:

  • register关键字建议编译器将变量存储在寄存器中,以便快速访问。它提供了一种提示,但并不保证编译器会将变量存储在寄存器中。这是因为寄存器的数量有限,编译器可能会根据优化策略和当前的寄存器分配情况来决定是否将变量存储在寄存器中。
  • register修饰符的使用场景通常是对频繁访问的变量进行优化,以减少内存访问的开销。例如,循环中的计数器变量可以使用register关键字修饰。
  • 下面是一个示例:

    #include <stdio.h>;
    
    int main() {
        register int count = 0;
        while (count &lt; 10) {
            printf("Count: %d\n", count);
            count++;
         }
    return 0;
    }
    

    上面的示例中,count变量被声明为register类型,这样编译器会将其存储在寄存器中,以提高循环中的访问效率。

    (2)volatile关键字:

  • volatile关键字告诉编译器该变量可能会在意料之外的情况下被修改,因此在编译器进行优化时应该避免对该变量进行过度优化。
  • volatile修饰符用于变量在多线程环境、硬件寄存器、中断服务程序等需要及时更新变量值的场景。因为这些情况下,变量的值可能由于外部因素而发生变化,而编译器通常无法感知到这些变化。
  • 下面是一个示例:

    #include <stdio.h>
    
    int main() {
        volatile int sensorValue = 0;
       while (1) {
           // 从传感器读取新值
           sensorValue = readSensor();
           // 将传感器值打印到控制台
           printf("Sensor Value: %d\n", sensorValue);
    }
    return 0;
    }
    

    上面的示例中,sensorValue变量被声明为volatile类型,这样就确保了每次循环中都会从传感器读取最新的值,并将其打印到控制台。由于传感器值的变化不受程序控制,编译器会避免对该变量进行优化,以确保每次循环都能获取最新的传感器值。
    需要注意的是,在使用register和volatile关键字时,应根据具体的嵌入式系统和编译器对其支持和行为进行调查,在适当的地方使用它们,以确保代码的正确性和性能优化。

    作者:fw拜拜

    物联沃分享整理
    物联沃-IOTWORD物联网 » 学习STM32单片机编程入门指南

    发表回复