“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))
- 将 addr 高四位(28~31)设为1。第25位+1。
- 只保留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为单位。