八段数码管显示解析

“8段数码管显示”工程解析

现象:数码管显示“12345678”。

源代码如下:

int main()
{
    Stm32_Clock_Init( 6 );  //传入一个8位二进制数,是倍频系数,使得PLL倍频。也对时钟进行了使能。
    delay_init( 72 );       //延时初始化,就是对系统滴答定时器的初始化。设置了延时倍乘数。
    LED_Init();             //LED使能和初始化。
    LED_SEL = 0;
    show_w1=1;
    show_w2=2;
    show_w3=3;
    show_w4=4;
    show_w5=5;
    show_w6=6;
    show_w7=7;
    show_w8=8;
    while(1)
    {

        SetLed(0, show_w1%10);
        delay_ms(1);
        SetLed(1, show_w2%10);
        delay_ms(1);
        SetLed(2, show_w3%10);
        delay_ms(1);
        SetLed(3, show_w4%10);
        delay_ms(1);
        SetLed(4, show_w5%10);
        delay_ms(1);
        SetLed(5, show_w6%10);
        delay_ms(1);
        SetLed(6, show_w7%10);
        delay_ms(1);
        SetLed(7, show_w8%10);
        delay_ms(1);
    }
}

此为库函数版。

库函数解析:

Stm32_Clock_Init();

//定义
//时钟初始化
void Stm32_Clock_Init(u8 PLL)//传入一个8位的二进制数
{
    unsigned char temp=0;   
    MYRCC_DeInit();       //复位并配置向量表
    RCC->CR|=0x00010000;  //外部高速时钟使能HSEON
    /*
    HSEON:外部高速时钟使能 (External high-speed clock enable) 
    由软件置’1’或清零。
    */
    while(!(RCC->CR>>17));//等待外部时钟就绪
    RCC->CFGR=0X00000400; //APB1=DIV2;APB2=DIV1;AHB=DIV1;
    PLL-=2;//抵消2个单位
    RCC->CFGR|=PLL<<18;   //设置PLL值 2~16
    RCC->CFGR|=1<<16;     //PLLSRC ON 
    FLASH->ACR|=0x32;     //FLASH 2个延时周期
    //repare

    RCC->CR|=0x01000000;  //PLLON
    while(!(RCC->CR>>25));//等待PLL锁定
    RCC->CFGR|=0x00000002;//PLL作为系统时钟    
    while(temp!=0x02)     //等待PLL作为系统时钟设置成功
    {   
        temp=RCC->CFGR>>2;
        temp&=0x03;
    }    
}            

总结:

传入一个8位二进制数,是倍频系数(范围2~16),使得PLL倍频。也对时钟进行了使能和初始化。配置了向量表,复位和配置了相关外设。

传参类型:u8

u8是unsigned char类型,8位二进制。

时钟树

HSE(High Speed External Clock signal):高速外部时钟信号

HSI(High Speed Internal Clock signal):高速内部时钟信号

LSE(Low Speed External Clock signal):低速外部时钟信号

LSI(Low Speed Internal Clock signal):低速内部时钟信号

USB预分频器:48MHz,连接USB。

APB1(低速)外设:UART2,TIMER2等。

APB2(高速)外设:UART1,SPI1等。

PLL锁相环:
作用:

​ 抬高频率。

输入:

​ HSE和HSI,通过PLLSRC选择(通过寄存器PLLCFGR的第22位决定:0:HSI;1:HSE)。

输出:

​ PLLCLK(系统时钟)和PLL48CLK。PLLCLK在多路选择开关处,根据需要传给SYSCLK。

MYRCC_DeInit(void)

//把所有时钟寄存器复位
void MYRCC_DeInit(void)
{                                                               
    RCC->APB1RSTR = 0x00000000;//复位结束             
    RCC->APB2RSTR = 0x00000000; 

    RCC->AHBENR = 0x00000014;  //睡眠模式闪存和SRAM时钟使能.其他关闭.      
    RCC->APB2ENR = 0x00000000; //外设时钟关闭.               
    RCC->APB1ENR = 0x00000000;   
    RCC->CR |= 0x00000001;     //使能内部高速时钟HSION                                                                 
    RCC->CFGR &= 0xF8FF0000;   //复位SW[1:0],HPRE[3:0],PPRE1[2:0],PPRE2[2:0],ADCPRE[1:0],MCO[2:0]                     
    RCC->CR &= 0xFEF6FFFF;     //复位HSEON,CSSON,PLLON
    RCC->CR &= 0xFFFBFFFF;     //复位HSEBYP          
    RCC->CFGR &= 0xFF80FFFF;   //复位PLLSRC, PLLXTPRE, PLLMUL[3:0] and USBPRE 
    RCC->CIR = 0x00000000;     //关闭所有中断
    //配置向量表    

    #ifdef  VECT_TAB_RAM
    MY_NVIC_SetVectorTable(NVIC_VectTab_RAM, 0x0);
    #else   
    MY_NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x0);
    #endif
    //配置中断向量表基址和偏移量
}

实际上是设置了向量表的复位,外设的复位,关闭所有中断。。

RCC结构体
/*------------- Reset and Clock Control --------------*/
typedef struct
{
  vu32 CR;        //时钟控制寄存器
  vu32 CFGR;    //时钟配置寄存器
  vu32 CIR;        //时钟中断寄存器
  vu32 APB2RSTR;//APB2 外设复位寄存器
  vu32 APB1RSTR;//APB1 外设复位寄存器
  vu32 AHBENR;    //AHB外设时钟使能寄存器
  vu32 APB2ENR;    //APB2 外设时钟使能寄存器
  vu32 APB1ENR;    //APB1 外设时钟使能寄存器
  vu32 BDCR;    //备份域控制寄存器
  vu32 CSR;        //控制/状态寄存器
} RCC_TypeDef;    

向量表的定义。

vu32(无符号长整型)
typedef volatile unsigned long  vu32;

注意到RCC结构体中寄存器的格式是 vu32 。vu32 即 volatile unsigned long

volatile即“易变的”。

C++会对代码进行优化,内部不影响IO的代码并不会被编译。

int main() {
    int i = 0;
    i++;
    cout << "hello world" << endl;
}

按照代码,这个程序会在内存中预留int大小的空间,初始化这段内存为0,然后这段内存中的数据加1,最后输出“hello world”到标准输出中。

但是根据这段代码编译出来的程序(加-O2选项),不会预留int大小的内存空间,更不会对内存中的数字加1。他只会输出“hello world”到标准输出中。

编译器为了优化代码,修改了程序的逻辑。实际上C++标准是允许写出来的代码和实际生成的程序不一致的。

虽说优化代码是件好事情,但是也不能让编译器任意修改程序逻辑,不然的话我们没办法写可靠的程序了。所以C++对这种逻辑的改写是有限制的,这个限制就是在编译器修改逻辑后,程序对外界的IO依旧是不变的。

怎么理解呢?实际上我们可以把我们写出来的程序看做是一个黑匣子,如果按照相同的顺序输入相同的输入,他就每次都会以同样的顺序给出同样的输出。这里的输入输出包括了标准输入输出、文件系统、网络IO、甚至一些system call等等,所有程序外部的事物都包含在内。

所以对于程序使用者来说,只要两个黑匣子的输入输出是完全一致的,那么这两个黑匣子是一致的,所以编译器可以在这个限制下任意改写程序的逻辑。这个规则又叫as-if原则。

volatile关键字的作用

volatile让该变量的地位相当于输入输出,不能改变顺序,不能忽略。所以类似取消优化。

RCC->CR|=0x00010000;

第16位:外部高速时钟使能HSEON

HSEON:外部高速时钟使能 (External high-speed clock enable)

由软件置’1’或清零。

while(!(RCC->CR>>17));

RCC_CR中第17位是HSERDY,即外部时钟就绪位,当外部时钟就绪时,HSERDY位就等于1了,RCC-CR>>17语句将第17位移到第0位,于是RCC-CR就等于0x00000001了,(!(RCC-CR>>17))值即为0了,while(!(RCC-CR>>17))就执行完了。

RCC->CFGR=0X00000400;

CFGR的第10位设为1。

PPRE1[2:0]:低速APB预分频(APB1) (APB low-speed prescaler (APB1))

由软件置’1’或清’0’来控制低速APB1时钟(PCLK1)的预分频系数。

警告:软件必须保证APB1时钟频率不超过36MHz。

0xx:HCLK不分频

100:HCLK 2分频

101:HCLK 4分频

110:HCLK 8分频

111:HCLK 16分频

此处是置为100,即HCLK2分频。

PLL-=2; RCC->CFGR|=PLL<<18;

此两步是设置了倍频系数,倍频系数设置如下图

RCC->CFGR|=1<<16;

设置了CFGR第16位。

PLLSRC:PLL输入时钟源 (PLL entry clock source)

由软件置’1’或清’0’来选择PLL输入时钟源。

只能在关闭PLL时才能写入此位。

0:HSI振荡器时钟经2分频后作为PLL输入时钟

1:HSE时钟作为PLL输入时钟。

RCC->CR|=0x01000000;

第24位置1,作用是PLL使能。

PLLON:PLL使能 (PLL enable)

由软件置’1’或清零。

当进入待机和停止模式时,该位由硬件清零。

当PLL时钟被用作或被选择将要作为系统时钟 时,该位不能被清零。

0:PLL关闭;

1:PLL使能。

while(!(RCC->CR>>25));

等待PLL时钟锁定。

PLLRDY:PLL时钟就绪标志 (PLL clock ready flag)

PLL锁定后由硬件置’1’。

0:PLL未锁定;

1:PLL锁定。

RCC->CFGR|=0x00000002;

PLL输出作为系统时钟;

SW[1:0]:系统时钟切换 (System clock switch)

由软件置’1’或清’0’来选择系统时钟源。

在从停止或待机模式中返回时或直接或间接作为系统时钟的HSE出现故障时,由硬件强制选择 HSI作为系统时钟(如果时钟安全系统已经启动)

00:HSI作为系统时钟;

01:HSE作为系统时钟;

10:PLL输出作为系统时钟;

11:不可用

delay_init();

延时初始化,就是对系统滴答定时器的初始化。设置了延时倍乘数。

传参:8位二进制数。

推挽输出

推挽输出既可以输出低电平,也可以输出高电平,可以直接驱动功耗不大的数字器件。可以理解成,输出0,1。

CPU 写 1,外部引脚输出高电平。

CPU 写 0,外部引脚输出低电平。

开漏输出

CPU 写 0,外部引脚接地。

CPU 写 1,外部引脚悬空。

LED_Init()

LED初始化。

LED_SEL = 0;

总结:

操作 用 GPIOB_ODR_Addr 和 n 初始化的地址 的内存空间。n = 3 。赋值为0

BITBAND(addr, bitnum)

#define BITBAND(addr, bitnum) ((addr & 0xF000 0000)+0x200 0000+((addr &0xFFFFF)<<5)+(bitnum<<2)) 
  1. 将 addr 高四位(28~31)设为1。第25位+1。
  2. 只保留addr的低20位(0-19),左移5位(5-24)。加上bitnum左移2位的数值。

两步相加。为BITBAND(addr, bitnum)。是一个初始化的地址。

MEM_ADDR(addr)

#define MEM_ADDR(addr)  *((volatile unsigned long  *)(addr)) 

MEM_ADDR(addr)表示addr地址处的值。

BIT_ADDR(addr, bitnum)

#define BIT_ADDR(addr, bitnum)   MEM_ADDR(BITBAND(addr, bitnum))

BIT_ADDR(addr, bitnum) 表示用addr和bitnum初始化的地址的值。

PBout(n)

#define PBout(n)   BIT_ADDR(GPIOB_ODR_Addr,n)

表示用 GPIOB_ODR_Addr 和 n 初始化的地址的值。或者说取地址处的内存空间进行操作。

GPIOB_ODR_Addr是 (GPIOB_BASE+12) 即 0x40010C0C 。n仍然是一个“偏移量”,实际上是寄存器内的“地址”。

什么是偏移量?

​ 例如GPIOA_BASE = (APB2PERIPH_BASE + 0x0800) 。而 GPIOA 又包含几个寄存器。

​ 所以每个寄存器都有一定偏移量。相当于寄存器在这个 GPIO 中的位置。

LED_SEL

#define LED_SEL PBout(3) 

即 n = 3 ;

show_w

定义:无符号字符类型,8位二进制。

void SetLed(u8 w, u8 value)

void SetLed(u8 w, u8 value)
{
    SEL0 = w%2;             //取w第0位
    SEL1 = w/2%2;           //取w第1位
    SEL2 = w/4;             //取w第2位
    LedValue(segTable[value]);
}
#define SEL0 PBout(0)
#define SEL1 PBout(1)
#define SEL2 PBout(2)
/***************************数码管段选***************************/
u8 segTable[] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};
void LedValue(u8 value)
{
    GPIOE->ODR &= ~(0xff<<8);       //清除数据,只保留低8位
    GPIOE->ODR |= value<<8;         //设置8~15位
}

SetLed 函数传入两个参数,第一个是位选,第二个是段选

void delay_ms(u16 nms)

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

延时函数,以ms为单位。