STM32基础学习(跑马灯实验,蜂鸣器实验)
一.C中的内存划分
在大多数单片机系统中,程序运行时有两类物理存储器:
-
FLASH(闪存)(非易失):存储代码和只读数据,掉电不丢失。
-
RAM(随机存取存储器)(易失):存储可读写的变量和堆栈,掉电丢失。
内存通常分为ROM(read only memory)和RAM (random access memory)。
其中,栈区、堆区以及静态区(ZI 段/RW段)是存在于RAM中的;
常量区和代码区存在于ROM中。
Data(数据)段:
包括RW段与BSS段(ZI段),BSS = ZI。
RW段存放的是已初始化的全局变量和静态变量;
BSS段存放的是未初始化的全局变量和静态变量。
Data段也可称为静态区(全局区)。
RO段:
代表只读段,存放的是常量数据, 如const修饰的全局变量。
Code段:
存放程序的可执行代码。
堆区:
用于动态分配内存,如malloc函数 所分配的内存存储在堆区;由程序员手动管理内存分配和释放。
栈区:
存放局部变量,形参,返回地址等;由系统自动分配内存和释放内存。
二.STM32系统架构图

STM32 主系统主要由四个驱动单元和四个被动单元构成。
四个驱动单元是:
内核DCode总线; 系统总线; 通用DMA1; 通用DMA2;
四个被动单元是:
AHB到APB的桥; 连接所有的APB设备; 内部FlASH闪存; 内部SRAM; FSMC;
① ICode总线:该总线将M3内核指令总线和闪存指令接口相连,指令的预取在该总线上 面完成。
② DCode总线:该总线将M3内核的DCode总线与闪存存储器的数据接口相连接,常量 加载和调试访问在该总线上面完成。
③ 系统总线:该总线连接M3内核的系统总线到总线矩阵,总线矩阵协调内核和DMA间 访问。
④ DMA总线:该总线将DMA的AHB主控接口与总线矩阵相连,总线矩阵协调CPU的 DCode 和DMA到SRAM,闪存和外设的访问。
⑤ 总线矩阵:总线矩阵协调内核系统总线和 DMA 主控总线之间的访问仲裁,仲裁利用 轮换算法。
⑥ AHB/APB桥:这两个桥在AHB和2个APB总线间提供同步连接,APB1操作速度限于 36MHz,APB2 操作速度全速。
三.STM32 时钟系统

在STM32中,有五个时钟源,为HSI、HSE、LSI、LSE、PLL。
从时钟频率来分可以分为 高速时钟源和低速时钟源。
在这5个中HIS,HSE以及PLL是高速时钟,LSI和LSE是低速时钟。
从来源可分为外部时钟源和内部时钟源,外部时钟源就是从外部通过接晶振的方式获取时钟源,其中HSE和LSE是外部时钟源,其他的是内部时钟源。
下面我们看看STM32的5个时钟源,我们讲解顺序是按图中红圈标示的顺序:
①、HSI是高速内部时钟,RC振荡器,频率为8MHz。
②、HSE 是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为 4MHz~16MHz。我们的开发板接的是8M的晶振。
③、LSI是低速内部时钟,RC振荡器,频率为40kHz。独立看门狗的时钟源只能是LSI,同 时LSI还可以作为RTC的时钟源。
④、LSE是低速外部时钟,接频率为32.768kHz的石英晶体。这个主要是RTC的时钟源。
⑤、PLL 为锁相环倍频输出,其时钟输入源可选择为HSI/2、HSE或者HSE/2。倍频可选择为 118
2~16 倍,但是其输出频率最大不得超过72MHz。
上面我们简要概括了STM32 的时钟源,那么这5个时钟源是怎么给各个外设以及系统提供时钟的呢? 图中我们用A ~E标示我们要讲解的地方。
A. B. C. D. E. MCO 是STM32 的一个时钟输出IO(PA8),它可以选择一个时钟信号输出,可以 选择为PLL输出的2分频、HSI、HSE、或者系统时钟。这个时钟可以用来给外 部其他系统提供时钟源。 这里是RTC时钟源,从图上可以看出,RTC的时钟源可以选择LSI,LSE,以及 HSE 的128分频。 从图中可以看出C处USB的时钟是来自PLL时钟源。
STM32中有一个全速功能 的USB模块,其串行接口引擎需要一个频率为48MHz的时钟源。该时钟源只能 从PLL输出端获取,可以选择为1.5分频或者1分频,也就是,当需要使用USB 模块时,PLL必须使能,并且时钟频率配置为48MHz或72MHz。 D处就是STM32的系统时钟SYSCLK,它是供STM32中绝大部分部件工作的时 钟源。系统时钟可选择为PLL输出、HSI或者HSE。系统时钟最大频率为72MHz, 当然你也可以超频,不过一般情况为了系统稳定性是没有必要冒风险去超频的。
这里的E处是指其他所有外设了。从时钟图上可以看出,其他所有外设的时钟最 终来源都是SYSCLK。SYSCLK 通过AHB分频器分频后送给各模块使用。
这些 模块包括:
①、AHB总线、内核、内存和DMA使用的HCLK时钟。
②、通过8分频后送给Cortex的系统定时器时钟,也就是systick了。
③、直接送给Cortex的空闲运行时钟FCLK。
④、送给APB1分频器。APB1分频器输出一路供APB1外设使用(PCLK1,最大 频率36MHz),另一路送给定时器(Timer)2、3、4倍频器使用。
⑤、送给APB2分频器。APB2分频器分频输出一路供APB2外设使用(PCLK2, 最大频率72MHz),另一路送给定时器(Timer)1倍频器使用。
其中需要理解的是APB1和APB2的区别,APB1上面连接的是低速外设,包括电源接口、 备份接口、CAN、USB、I2C1、I2C2、UART2、UART3等等,APB2上面连接的是高速外设包 括UART1、SPI1、Timer1、ADC1、ADC2、所有普通IO口(PA~PE)、第二功能IO口等。
APB2下面所挂的外设的时 钟要比APB1的高。
这里总结一下SystemInit()函数中设置的系统时钟大小:
SYSCLK(系统时钟) =72MHz
AHB总线时钟(使用SYSCLK) =72MHz
APB1总线时钟(PCLK1) =36MHz
APB2总线时钟(PCLK2) =72MHz
PLL时钟 =72MHz
以上关于时钟内容看的不是很明白,之后再来继续学习。
四.MDK下的C基础
1. 位操作
C 语言支持如下6中位操作

1) 不改变其他位的值的状况下,对某几个位进行设值。
这个场景单片机开发中经常使用,方法就是先对需要设置的位用&操作符进行清零操作, 然后用|操作符设值。比如我要改变GPIOA的状态,可以先对寄存器的值进行&清零操作
GPIOA->CRL&=0XFFFFFF0F; //将第 4-7 位清 0 然后再与需要设置的值进行|或运算
GPIOA->CRL|=0X00000040; //设置相应位的值,不改变其他位的值
2) 移位操作提高代码的可读性。
移位操作在单片机开发中也非常重要,下面让我们看看固件库的GPIO初始化的函数里 面的一行代码
GPIOx->BSRR = (((uint32_t)0x01) BSRR =0x0030;
这样的代码就不好看也不好重用了。 类似这样的代码很多:
GPIOA->ODR|=1<<5; //PA.5 输出高,不改变其他位
这样我们一目了然,5告诉我们是第5位也就是第6个端口,1告诉我们是设置为1了。
3) ~取反操作使用技巧 SR 寄存器的每一位都代表一个状态,某个时刻我们希望去设置某一位的值为0,同时 其他位都保留为1,简单的作法是直接给寄存器设置一个值:
TIMx->SR=0xFFF7;
这样的作法设置第3位为0,但是这样的作法同样不好看,并且可读性很差。看看库函数 代码中怎样使用的:
TIMx->SR = (uint16_t)~TIM_FLAG;
而TIM_FLAG 是通过宏定义定义的值:
#define TIM_FLAG_Update ((uint16_t)0x0001)
#define TIM_FLAG_CC1 ((uint16_t)0x0002)
看这个应该很容易明白,可以直接从宏定义中看出TIM_FLAG_Update就是设置的第0位了, 可读性非常强。
2.define 宏定义
define 是C 语言中的预处理命令,它用于宏定义,可以提高源代码的可读性,为编程提供 方便。常见的格式: #define 标识符 字符串 “标识符”为所定义的宏名。“字符串”可以是常数、表达式、格式串等。
例如: #define SYSCLK_FREQ_72MHz 72000000 定义标识符SYSCLK_FREQ_72MHz的值为72000000。
3. ifdef 条件编译
单片机程序开发过程中,经常会遇到一种情况,当满足某条件时对一组语句进行编译,而 当条件不满足时则编译另一组语句。
条件编译命令最常见的形式为:
#ifdef 标识符
程序段1
#else 程序段2
#endif
它的作用是:当标识符已经被定义过(一般是用#define命令定义),则对程序段1进行编译, 否则编译程序段2。 其中#else部分也可以没有,即:
#ifdef
程序段1
#endif
五.SYSTEM文件夹介绍
1.delay 文件夹代码介绍;
delay 文件夹内包含了delay.c和delay.h 两个文件,这两个文件用来实现系统的延时功能, 其中包含7个函数:
void delay_osschedlock(void);
void delay_osschedunlock(void);
void delay_ostimedly(u32 ticks);
void SysTick_Handler(void);
void delay_init(void);
void delay_ms(u16 nms);
void delay_us(u32 nus);
前面4个函数,仅在支持操作系统(OS)的时候,需要用到,而后面三个函数,则不论是 否支持OS都需要用到。 在介绍这些函数之前,我们先了解一下delay延时的编程思想:CM3内核的处理器,内部 包含了一个 SysTick 定时器,SysTick 是一个 24 位的倒计数定时器,当计数到 0 时,将从 RELOAD 寄存器中自动重装载定时初值,开始新一轮计数。只要不把它在 SysTick 控制及状 态寄存器中的使能位清除,就永不停息。就是利用STM32的内部SysTick来实现延时的,这样既不占用中断,也不占用系统定时器。
2.sys 文件夹代码介绍;
sys 文件夹内包含了sys.c和sys.h两个文件。在sys.h里面定义了STM32的IO口输入读取 宏定义和输出宏定义。sys.c里面只定义了一个中断分组函数。
IO 口的位操作实现
该部分代码在sys.h文件中,实现对STM32各个IO口的位操作,包括读入和输出。当然 在这些函数调用之前,必须先进行IO口时钟的使能和IO口功能定义。
此部分仅仅对IO口进 行输入输出读取和控制。 位带操作简单的说,就是把每个比特膨胀为一个32位的字,当访问这些字的时候就达到了 访问比特的目的,比如说BSRR寄存器有32个位,那么可以映射到32个地址上,我们去访问 这32个地址就达到访问32个比特的目的。这样我们往某个地址写1就达到往对应比特位写1 的目的,同样往某个地址写0就达到往对应的比特位写0的目的。
3.usart 文件夹代码介绍;
usart 文件夹内包含了 usart.c 和 usart.h 两个文件。这两个文件用于串口的初始化和中断接 收。这里只是针对串口1,比如你要用串口2或者其他的串口,只要对代码稍作修改就可以了。 usart.c里面包含了2个函数一个是void USART1_IRQHandler(void);另外一个是void uart_init(u32 bound);里面还有一段对串口 printf 的支持代码,如果去掉,则会导致 printf 无法使用,虽然软 件编译不会报错,但是硬件上STM32是无法启动的,这段代码不要去修改。
关于上面这三个文件夹略写,后面学到了再去补充。
实验一:跑马灯实验
1.工程结构说明
📁 Start(启动与核心层)
存放芯片启动文件和底层核心代码:
| 文件 | 作用 |
|---|---|
startup_stm32f10x_md.s |
启动文件(汇编),定义中断向量表、堆栈初始化、程序入口 |
core_cm3.c/h |
Cortex-M3 内核接口,提供 NVIC、SysTick 等内核级功能访问 |
stm32f10x.h |
寄存器头文件,定义所有外设寄存器的地址和结构体 |
system_stm32f10x.c/h |
系统初始化,配置时钟树(SYSCLK、AHB、APB)、PLL 等 |
📁 Library(标准外设库)
通常存放 ST 官方提供的 标准外设库(Standard Peripheral Library) 源文件,如:
stm32f10x_gpio.cstm32f10x_usart.cstm32f10x_rcc.c- ...
截图中该文件夹是折叠状态,展开后会有各个外设的驱动源文件。
📁 System(系统工具层)
用户自定义的系统级功能模块:
| 文件 | 作用 |
|---|---|
Delay.c/h |
延时函数,实现微秒/毫秒级延时(通常基于 SysTick 定时器) |
📁 User(用户应用层)
存放用户编写的应用代码:
| 文件 | 作用 |
|---|---|
main.c |
主程序入口,包含 main() 函数和用户逻辑 |
stm32f10x_conf.h |
库配置文件,选择需要哪些外设库头文件(通过 #include 控制) |
stm32f10x_it.c/h |
中断服务程序,存放所有外设的中断处理函数(如 USART1_IRQHandler、TIM2_IRQHandler 等) |
2. 硬件设计

3. 软件设计
跑马灯实验我们主要用到的固件库文件是:
stm32f10x_gpio.c /stm32f10x_gpio.h stm32f10x_rcc.c/
stm32f10x_rcc.h misc.c/ misc.h stm32f10x_usart /
stm32f10x_usart.h
其中stm32f10x_rcc.h 头文件在每个实验中都要引入,因为系统时钟配置函数以及相关的外设时 钟使能函数都在这个其源文件 stm32f10x_rcc.c 中。stm32f10x_usart.h 和 misc.h 头文件在我们 SYSTEM文件夹中都需要使用到,所以每个实验都会引用。
实验代码如下:
#include "stm32f10x.h" // Device header
#include "Delay.h"
int main(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
//使用各个外设前必须开启时钟,否则对外设的操作无效
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure; //定义结构体变量
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //GPIO模式,赋值为推挽输出模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All; //GPIO引脚,赋值为所有引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //GPIO速度,赋值为50MHz
GPIO_Init(GPIOA, &GPIO_InitStructure); //将赋值后的构体变量传递给GPIO_Init函数
//函数内部会自动根据结构体的参数配置相应寄存器
//实现GPIOA的初始化
/*主循环,循环体内的代码会一直循环执行*/
while (1)
{
/*使用GPIO_Write,同时设置GPIOA所有引脚的高低电平,实现LED流水灯*/
GPIO_Write(GPIOA, ~0x0001); //0000 0000 0000 0001,PA0引脚为低电平,其他引脚均为高电平,注意数据有按位取反
Delay_ms(100); //延时100ms
GPIO_Write(GPIOA, ~0x0002); //0000 0000 0000 0010,PA1引脚为低电平,其他引脚均为高电平
Delay_ms(100); //延时100ms
GPIO_Write(GPIOA, ~0x0004); //0000 0000 0000 0100,PA2引脚为低电平,其他引脚均为高电平
Delay_ms(100); //延时100ms
GPIO_Write(GPIOA, ~0x0008); //0000 0000 0000 1000,PA3引脚为低电平,其他引脚均为高电平
Delay_ms(100); //延时100ms
GPIO_Write(GPIOA, ~0x0010); //0000 0000 0001 0000,PA4引脚为低电平,其他引脚均为高电平
Delay_ms(100); //延时100ms
GPIO_Write(GPIOA, ~0x0020); //0000 0000 0010 0000,PA5引脚为低电平,其他引脚均为高电平
Delay_ms(100); //延时100ms
GPIO_Write(GPIOA, ~0x0040); //0000 0000 0100 0000,PA6引脚为低电平,其他引脚均为高电平
Delay_ms(100); //延时100ms
GPIO_Write(GPIOA, ~0x0080); //0000 0000 1000 0000,PA7引脚为低电平,其他引脚均为高电平
Delay_ms(100); //延时100ms
}
}
大多数 STM32 开发板的 LED 采用 低电平点亮(共阳极接法):
- GPIO 输出低电平(0) → LED 两端有电压差 → LED 亮
- GPIO 输出高电平(1) → LED 两端无电压差 → LED 灭
实验二:蜂鸣器实验
1 蜂鸣器简介
蜂鸣器是一种一体化结构的电子讯响器,采用直流电压供电,广泛应用于计算机、打印机、 复印机、报警器、电子玩具、汽车电子设备、电话机、定时器等电子产品中作发声器件。蜂鸣 器主要分为压电式蜂鸣器和电磁式蜂鸣器两种类型。

这里的有源不是指电源的“源”,而是指有没有自带震荡电路,有源蜂鸣器自带了震荡电路, 一通电就会发声;无源蜂鸣器则没有自带震荡电路,必须外部提供2~5Khz 左右的方波驱动, 才能发声。
2 硬件设计

3 软件设计
#include "stm32f10x.h" // Device header
#include "Delay.h"
int main(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
//使用各个外设前必须开启时钟,否则对外设的操作无效
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure; //定义结构体变量
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //GPIO模式,赋值为推挽输出模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_12; //GPIO引脚,赋值为第12号引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //GPIO速度,赋值为50MHz
GPIO_Init(GPIOB, &GPIO_InitStructure); //将赋值后的构体变量传递给GPIO_Init函数
//函数内部会自动根据结构体的参数配置相应寄存器
//实现GPIOB的初始化
/*主循环,循环体内的代码会一直循环执行*/
while (1)
{
GPIO_SetBits(GPIOB, GPIO_Pin_0);
Delay_ms(100); //将PA0引脚设置为高电平
GPIO_ResetBits(GPIOB, GPIO_Pin_12); //将PB12引脚设置为低电平,蜂鸣器鸣叫
Delay_ms(100); //延时100ms
GPIO_SetBits(GPIOB, GPIO_Pin_12); //将PB12引脚设置为高电平,蜂鸣器停止
Delay_ms(100); //延时100ms
GPIO_ResetBits(GPIOB, GPIO_Pin_12); //将PB12引脚设置为低电平,蜂鸣器鸣叫
Delay_ms(100); //延时100ms
GPIO_SetBits(GPIOB, GPIO_Pin_12); //将PB12引脚设置为高电平,蜂鸣器停止
Delay_ms(700);
GPIO_ResetBits(GPIOB, GPIO_Pin_0);
Delay_ms(100);
}
}
更多推荐


所有评论(0)