第一节 STM32F103C8T6最小系统
1. 复位电路
单片机在正常运行时,由于外界干扰等因素可能会使单片机程序陷入死循环状态或"跑飞"状态。若要使其进入正常状态,唯一的办法是将单片机复位,以重新启动。复位就是把单片机当前的运行状态恢复到起始状态的操作,其作用是复位单片机的程序计数器PC,使单片机从代码存储器0x00000000单元重新开始执行程序,并将相关寄存器复位到默认初始值

- 开关:C318938
- 电容:C61119867
- 电阻:C25805

- 这个芯片是:STM32F103C8T6
2. 时钟电路
晶振电路为单片机提供时钟信号。STM32有两组晶振

OSC_IN/OSC_OUT: 外部HSE晶振引脚,用于给STM32提供高精度系统时钟。高速外部时钟(HSE)可以使用一个4~16MHz的晶体/陶瓷谐振器构成的振荡器产生。为了程序内部倍频方便,一般选用8MHz的晶振
**注意:**在PCB布线时晶振应尽量靠近芯片。晶振底部不要进行布线,防止其他信号干扰晶振
准备电子元器件:
- 电容:C1648 高速外部晶振:C21263 低速外部晶振:C2236


3. boot启动电路
在STM32单片机里,可以通过BOOT[1:0]引脚选择三种不同启动模式,如下所示:

准备电子元器件: 电阻:C23162 开关:C318938
注意: BOOT1对应芯片上的引脚位置



4. 滤波电容/添加电源



5. swd调试



6. 5v转3.3
7. usb转串口
TYPE-C
- 这个5v是我们从电脑输入板子的,就是那根板子和电脑连接的线
- 但这个元器将还要连个小芯片
CH340

- 输入输出交叉连接

- 加上转接符表示,这根线即可以当串口,又可以当GPIO使用

- 这2根线需要根芯片上的那2个反着连

- 加上这个二极管表示,防止数据回流

8. 引出导线
9. 添加按键与LED

第二节 了解GPIO/STM32 芯片
GPIO(general purpose intput output)是通用输入输出端口的简称,可以通过软件来控制其输入和输出。STM32 芯片的 GPIO 引脚与外部设备连接起来,从而实现与外部通讯、控制以及数据采集的功能 。不过GPIO 最简单的应用还属点亮 LED 灯了,只需通过软件控制 GPIO 输出高低电平即可。当然GPIO 还可以作为输入控制,比如在引脚上接入一个按键,通过电平的高低判断按键是否按下。我们开发板上使用的 STM32 型号是 STM32F103C8T6,此芯片共有48 引脚,芯片引脚图
如下图所示 :

那么是不是所有引脚都是 GPIO 呢?当然不是,STM32 引脚可以分为这么几大类:
- 电源引脚:引脚图中的 VDD、VSS、VSSA、VDDA 等都属于电源引脚。
- 晶振引脚:引脚图中的 PC14、PC15 和 OSC_IN、OSC_OUT 都属于晶振引脚,不过它们还可以作为普通引脚使用。
- 复位引脚:引脚图中的 NRST 属于复位引脚,不做其他功能使用。
- 下载引脚:引脚图中的 PA13、PA14、PA15、PB3 和PB4 属于JTAG或SWD 下载引脚。不过它们还可以作为普通引脚或者特殊功能使用,具体的功能可以查看芯片数据手册,里面都会有附加功能说明。当然,STM32 的串口功能引脚也是可以作为下载引脚使用。
- BOOT 引脚:引脚图中的 BOOT0 和 PB2(BOOT1)属于BOOT 引脚,PB2还可以作为普通管脚使用。在 STM32 启动中会有模式选择,其中就是依靠着BOOT0和 BOOT1 的电平来决定。 (6)GPIO 引脚:引脚图中的 PA、PB、PC、PD 等均属于GPIO 引脚。从引脚图可以看出,GPIO 占用了 STM32 芯片大部分的引脚。并且每一个端口都有16个引脚,比如 PA 端口,它有 PA0-PA15。其他的 PB 等端口是一样的。
1. GPIO的结构框图


2. GPIO的主要特点
- 不同型号,IO口的数量可能不一样。
- 快速翻转。最快可以达到每2个时钟周期翻转一次。(STM32F1系列最快可以达到50MHz的翻转速度)。
- 每个IO都可以作为外部中断。
- 支持8种工作模式
3. GPIO的8种工作模式
GPIO端口的每个位(引脚)可以由软件分别配置成8种模式,当然对同一个引脚同一时间只能处于某一种模式中
输入浮空(Input floating)

说明一下:
- 如果VDD上面的那个开关闭合了,就是输入上拉(Input pull-up),此时内部是高电压
- 如果VSS下面的那个开关闭合了,就是输入下拉(Input-pull-down),此时内部是低电压
- 模拟输入(Analog)一般和ADC/DAC有关
处理输入,还有输出:

- 通用开漏输出(Output open-drain)
- 通用推挽式输出(Output push-pull)
- 推挽式复用功能(Alternate function push-pull)
- 开漏复用功能(Alternate function open-drain)
4. 与GPIO相关的7个寄存器
每个GPIO端口有7个相关的寄存器:
- Ø 2个32位配置寄存器(GPIOx_CRL,GPIOx_CRH)
- Ø 2个32位数据寄存器(GPIOx_IDR和GPIOx_ODR)
- Ø 1个32位置位/复位寄存器(GPIOx_BSRR)
- Ø 1个16位复位寄存器(GPIOx_BRR)
- Ø 1个32位锁定寄存器(GPIOx_LCKR
4.1 GPIOx_CR端口配置低寄存器
GPIOx_CRL(Port configuration register low),x可以是A-G

该寄存器配置的每个GPIO的 0-7 这个8个位,所以叫低寄存器
4.2 GPIOx_CRH端口配置高寄存器
GPIOx_CRH(Port configuration register high)。
该寄存器配置的是每个端口的 8-15引脚,配置方式和低位寄存器完全一
4.3 GPIOx_IDR端口输入数据寄存器
Port input data register

保留位始终读为0。剩下的分别对应每个引脚的输入值
4.4 GPIOx_ODR端口输出数据寄存器
Port output data register

保留位始终读为0。剩下的分别对应每个引脚的输出值。
4.5 GPIOx_BSRR端口位设置/清除寄存器
Port bit set/reset register

(1)高16位是用清除对应的数据输出寄存器的位(0-15)的值:设置为0不影响,设置为1会清除ODR对应的位的值(置为0)。
(2)低16位是用设置对应的数据输出寄存器的位(0-15)的值:设置为0不影响,设置为1会设置ODR对应的位的值(置为1)
4.6 GPIOx_BRR端口位清除寄存器
这个寄存器具有了GPIOx_BSRR一半的功能:清除。

4.7 GPIOx_LCKR端口配置锁定寄存器
Port configuration lock register

该寄存器用来锁定端口位的配置。位[15:0]用于锁定GPIO端口的配置。在规定的写入操作期间,不能改变LCKR[15:0]。当对相应的端口位执行了LOCK序列后,在下次系统复位之前将不能再更改端口位的配置。
每个锁定位锁定控制寄存器(CRL,CRH)中相应的4个位(CNF2位和MODE2位)。
第16位用来激活锁定寄存器,必须按照规定的时序来操作才行: 写1 -> 写0 -> 写1 -> 读0 -> 读1。
对0-15位:
- Ø 0:不锁定对应端口的配置。
- Ø 1:锁定对应端口的配置
第三节 第一个LED灯 - 标准库版本
1. 给APB2 GPIOC 时钟使能

cpp
#include "stm32f10x.h"
int main()
{
//step1: 给APB GPIOC 时钟使能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
while(1){
}
return 0;
}
2. 将GPIO13的工作模式 设置为推挽输出50MHZ
cpp
#include "stm32f10x.h"
int main()
{
//step1: 给APB GPIOC 时钟使能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
//step2:将GPIO13的工作模式 设置为推挽输出50MHZ
GPIO_InitTypeDef initDef;
initDef.GPIO_Pin = GPIO_Pin_13;
initDef.GPIO_Speed = GPIO_Speed_50MHz;
initDef.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOC,&initDef);
while(1){
}
return 0;
}


3. 将GPIO13设置为底电平
cpp
#include "stm32f10x.h"
int main()
{
//step1: 给APB GPIOC 时钟使能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
//step2:将GPIO13的工作模式 设置为推挽输出50MHZ
GPIO_InitTypeDef initDef;
initDef.GPIO_Pin = GPIO_Pin_13;
initDef.GPIO_Speed = GPIO_Speed_50MHz;
initDef.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOC,&initDef);
//step3: 将GPIO13设置为底电平
//GPIO_SetBits(GPIOC,GPIO_Pin_13);// 设置高电平
//GPIO_ResetBites(GPIOC,GPIO_Pin_13);// 清除高电平
GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_RESET);
while(1){
}
return 0;
}
说明一下:
- GPIO_WriteBit的第三个参数为BIt_RESET时,为重置bit-低电平
- GPIO_WriteBit的第三个参数为BIt_SET时,为设置bit-高电平
第四节 了解时钟树
在STM32中有3种不同的时钟源用来驱动系统时钟(SYSCLK):
(1)HSI振荡器时钟(High Speed Internal oscillator,高速内部时钟)
(2)HSE振荡器时钟(High Speed External(Oscillator / Clock),高速外部时钟)
(3)PLL时钟(Phase Locked Loop 锁相环/倍频器)
还有2种2级时钟:
(4)LSI时钟(Low Speed Internal,低速内部时钟)
(5)LSE时钟(Low Speed External oscillator,低速外部时钟)
为什么提供这么多的时钟?**节能!**高速设备接高速时钟,低速设备接低速时钟,可以最大程度的达到节能效果。详见下图时钟树
时钟初始化配置函数: SystemInit()
时钟使能配置函数:
cpp
void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalState NewState);
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);
void RCC_APB1PeriphClockCmd(uint32_t RCC_APB1Periph, FunctionalState NewState);
修改系统时钟:RCC_HSE_Config()
注意: 不要把 STM32 系统时钟设置超过72M使用,否则很容易崩溃
第五节 位带操作
简单来说就是直接操作寄存器中得到bit位
修改对某引脚设置对于值时。有两种方案:
- 方案1: 调用对于库函数
- 方案2 : 通过位带操作
就比如说我们的第一个LED灯中的第3步:将GPIO13设置为底电平
本质上就是将那个寄存器中的第13位的bit位改成了0
1. 新建文件/文件夹



2. system.h
cpp
#ifndef _H_SYSTEM
#define _H_SYSTEM
#include "stm32f10x.h"
//IO 口地址映射
#define GPIOA_IDR_Addr (GPIOA_BASE+8) //0x40010808
#define GPIOB_IDR_Addr (GPIOB_BASE+8) //0x40010C08
#define GPIOC_IDR_Addr (GPIOC_BASE+8) //0x40011008
#define GPIOD_IDR_Addr (GPIOD_BASE+8) //0x40011408
#define GPIOE_IDR_Addr (GPIOE_BASE+8) //0x40011808
#define GPIOF_IDR_Addr (GPIOF_BASE+8) //0x40011A08
#define GPIOG_IDR_Addr (GPIOG_BASE+8) //0x40011E08
#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
//把 addr 地址强制转换为 unsigned long 类型的指针
#define MEM_ADDR(addr) *((volatile unsigned long *)(addr))
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr&0xFFFFF)<<5)+(bitnum<<2))
//把位带别名区内地址转换为指针 ,获取地址内的数据
#define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum))
//IO 口操作,只对单一的 IO 口!
//确保 n 的值小于 16!
#define PAin(n) BIT_ADDR(GPIOA_IDR_Addr,n) //输入
#define PBin(n) BIT_ADDR(GPIOB_IDR_Addr,n) //输入
#define PCin(n) BIT_ADDR(GPIOC_IDR_Addr,n) //输入
#define PDin(n) BIT_ADDR(GPIOD_IDR_Addr,n) //输入
#define PEin(n) BIT_ADDR(GPIOE_IDR_Addr,n) //输入
#define PFin(n) BIT_ADDR(GPIOF_IDR_Addr,n) //输入
#define PGin(n) BIT_ADDR(GPIOG_IDR_Addr,n) //输入
#define PAout(n) BIT_ADDR(GPIOA_ODR_Addr,n) //输出
#define PBout(n) BIT_ADDR(GPIOB_ODR_Addr,n) //输出
#define PCout(n) BIT_ADDR(GPIOC_ODR_Addr,n) //输出
#define PDout(n) BIT_ADDR(GPIOD_ODR_Addr,n) //输出
#define PEout(n) BIT_ADDR(GPIOE_ODR_Addr,n) //输出
#define PFout(n) BIT_ADDR(GPIOF_ODR_Addr,n) //输出
#define PGout(n) BIT_ADDR(GPIOG_ODR_Addr,n) //输出
#endif
说明一下: 知道怎么用就行了
3. main.c
cpp
#include "stm32f10x.h"
//#include "led.h"
#include "system.h"
#define LED1 PCout(13)
int main()
{
//step1: 给APB GPIOC 时钟使能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
//step2:将GPIO13的工作模式 设置为推挽输出50MHZ
GPIO_InitTypeDef initDef;
initDef.GPIO_Pin = GPIO_Pin_13;
initDef.GPIO_Speed = GPIO_Speed_50MHz;
initDef.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOC,&initDef);
//step3:设置为低电平
PCout(13) = 0;
while(1){
}
//return 0;
}
第六节 SysTick 系统定时器
SysTick(System Tick Timer)是 ARM Cortex-M 系列内核中集成的一个简单定时器,主要用于操作系统中的时间片轮转调度(如 RTOS 中的时基)。以下是 SysTick 系统定时器的原理简要说明:
1. 基本组成
SysTick 是一个 24 位 向下递减的计数器,包含以下几个寄存器:
-
LOAD:装载值寄存器,设定定时器的初始值(最大值为 0xFFFFFF)。
-
VAL:当前计数值寄存器。
-
CTRL:控制与状态寄存器,用于启动定时器、中断控制等。
-
CALIB:校准值寄存器(只读,用于提供基准时间,比如 1ms 对应多少个时钟周期)
2. 工作原理

-
启动后,SysTick 将
LOAD
中的值交给VAL寄存器开始递减。(多线程情况) -
每当计数器减到 0:
-
计数器会自动重装(重新从 LOAD 开始倒数)。
-
如果开启了中断,会产生一个中断信号(SysTick_Handler)。
-
-
中断可以用于实现周期性任务(如每 1ms 调用一次调度器或系统计时器
3. 操作步骤
SysTick 定时器的操作可以分为 4 步:
- 设置 SysTick 定时器的时钟源。
- 设置 SysTick 定时器的重装初始值(如果要使用中断的话,就将中断使能打开)。
- 清零SysTick 定时器当前计数器的值。
- 打开 SysTick 定时器

systick.h
cpp
#ifndef _H_SYSTICK
#define _H_SYSTICK
#include "stm32f10x.h"
#include "misc.h"
//初始化延迟函数
void SysTick_Init(u8 SYSCLK);
//延迟多少微秒
void delay_us(u32 us_num);
//延迟多少毫秒
void delay_ms(u16 ms_num);
#endif
SysTick_Init 函数形参 SYSCLK 表示的系统时钟大小,默认配置我们使用的系统时钟是 72M,所以调用这个函数时,形参值即为 72。函数内部调用了一个库函数 SysTick_CLKSourceConfig,此函数用来对 SysTick 定时器时钟的选择,我们使用的 SysTick 定时器时钟是系统时钟的8 分频 ,所以参数是SysTick_CLKSource_HCLK_Div8。如果使用系统时钟作为SysTick 定时器时钟,那么参数即为 SysTick_CLKSource_HCLK。这个函数在misc.c 库文件内,如何查找我们前面介绍过方法。
下面的一条语句是用来求取 SysTick 定时器在 1us 时间内和1ms 时间内的计数次数
systick.c
cpp
#include "systick.h"
u8 fac_us;
u16 fac_ms;
//初始化延迟函数
void SysTick_Init(u8 SYSCLK){
//设置时钟源
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);
//计算us,ms滴答次数
fac_us = SYSCLK/8;
fac_ms = SYSCLK/8*1000;
}
//延迟多少微秒
void delay_us(u32 us_num){
//初始加载
SysTick->LOAD = us_num*fac_us; //获取us_num时间内要技术次数
//初始值
SysTick->VAL = 0x00; //默认最开始位0
//开启计数器
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
u32 temp;
do{
temp = SysTick->CTRL;
//temp的第16为为1计数结束
}while(temp&0x01&&!(temp&1<<16));
//关闭计数器
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
//清空初始值
SysTick->VAL = 0x00;
}
//延迟多少毫秒
void delay_ms(u16 ms_num){
//初始加载
SysTick->LOAD = ms_num*fac_ms; //获取us_num时间内要技术次数
//初始值
SysTick->VAL = 0x00; //默认最开始位0
//开启计数器
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
u32 temp;
do{
temp = SysTick->CTRL;
//temp的第16为为1计数结束
}while(temp&0x01&&!(temp&1<<16));
//关闭计数器
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
//清空初始值
SysTick->VAL = 0x00;
}

说明一下:
- SysTick_CTRL_ENABLE_Msk就是1
4. main.c



