本次实验主要有以下两个任务,所以本博客也会按照此顺序进行讲解
- 学习和理解STM32F103系列芯片的地址映射和寄存器映射原理;了解GPIO端口的初始化设置三步骤(时钟配置、输入输出模式设置、最大速率设置)
- 以 STM32最小系统核心板(STM32F103C8T6)+面板板+3只红绿蓝LED 搭建电路,使用GPIO端口控制LED灯,轮流闪烁,间隔时长1秒。
目录
一、实验介绍
1. 试验器材介绍
- stm32f103c8t6系统最小版
- stlink-v2(根据自己需求也可以采用串口烧入方法)
- 面包板
- 杜邦线(含公对公、母对母)
- 红、绿、黄 三个发光二极管
- keil5软件
2. 实验理论介绍
芯片的地址映射和寄存器映射原理
存储器映射
存储器在产家制作完成后是一片没有任何信息的物理存储器,而CPU要进行访存就涉及到内存地址的概念,因此存储器映射就是为物理内存按一定编码规则分配地址的行为。值得注意,存储器映射一般是由产家规定,用户不能随意更改。
寄存器映射
寄存器映射是在存储器映射的基础上进行的。
以STM32为例,操作硬件本质上就是操作寄存器。在存储器片上外设区域,四字节为一个单元,每个单元对应不同的功能。当我们控制这些单元时就可以驱动外设工作,我们可以找到每个单元的起始地址,然后通过C 语言指针的操作方式来访问这些单元。但若每次都是通过这种方式访问地址,不好记忆且易出错。这时我们可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名实质上就是寄存器名字。给已分配好地址(通过存储器映射实现)的有特定功能的内存单元取别名的过程就叫寄存器映射。
如何找到芯片中寄存器的地址
如果想要找到芯片中某个寄存器的地址的话,就需要查看数据手册,并通过一定的计算,就可以找到寄存器的地址啦。
寄存器的地址分为基地址和偏移地址
详细计算方法可以参考博客:STM32寄存器的简介、地址查找,与直接操作寄存器
GPIO口初始化设置
GPIO口介绍
每个GPI/O端口有两个32位配置寄存器(GPIOx_CRL,GPIOx_CRH),
两个32位数据寄存器(GPIOx_IDR和GPIOx_ODR),一个32位置位/复位寄存器
根据数据手册中列出的每个I/O端口的特定硬件特征,GPIO端口的每个位可以由软件分别配置成多种模式。
- 输入浮空
- 输入上拉
- 输入下拉
- 模拟输入
- 开漏输出
- 推挽式输出
- 推挽式复用功能
- 开漏复用功能
GPIO口在哪里
参考芯片手册,在stm32f103c8t6中有A B C D E F G七个GPIO口,由上文提到的地址计算方法,可计算出寄存器地址,在通过储存器原理图可找到接口。
关于GPIO的更多地址资料可参考芯片的数据手册
(下载资料都以网盘的形式放在了文末)
初始化步骤
第一步:使能GPIOx口的时钟
第二步:指明GPIOx口的哪一位,这一位的速度大小以及模式。
第三步:调用GPIOx口初始化函数,进行初始化。
第四步:调用GPIO-SetBits函数,进行相应为的置位。
这篇博客中有详尽的关于GPIO初始化的实例与代码,供大家参考
STM32入门-GPIO初始化步骤
二、实验操作
实验操作分三部分
- 软件代码编写
- 硬件接线
- 烧入调试
其实网上的流水灯代码资料是比较多的,所以我可能对于代码的讲解不会很详尽,对于硬件接线一些上手操作可能会细点。
1. C语言库函数版
环境配置(库函数之类的东西)
这个里面涉及到了一个keil模块化编程的事,说着比较麻烦,但是学会了又很简单,建议百度找找其他博客学一下。
C语言源码
此程序是将A5 A6 A7作为输出口的
#include "stm32f10x.h"
void Delay(u32 count) //延迟函数,参数为设置的延迟时间(不准确,具体时间可由keil的示波器观察)
{
u32 i=0;
for(;i<count;i++);
}
int main(void)
{
GPIO_InitTypeDef GPIO_InitStructure; //设置PA5、PA6、PA7为输出口
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5|GPIO_Pin_6|GPIO_Pin_7; //GPIO口的初始化操作
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA,GPIO_Pin_5); //初始化GPIO为高电平,LED低电平亮,高电平灭
GPIO_SetBits(GPIOA,GPIO_Pin_6);
GPIO_SetBits(GPIOA,GPIO_Pin_7);
while(1) //永真循环,进行不断的流水灯循环闪烁
{
GPIO_ResetBits(GPIOA,GPIO_Pin_5);
GPIO_SetBits(GPIOA,GPIO_Pin_6);
GPIO_SetBits(GPIOA,GPIO_Pin_7);
Delay(4600000);Delay(4600000);Delay(4600000); //流水灯中,由延时函数控制时间间隔
GPIO_SetBits(GPIOA,GPIO_Pin_5);
GPIO_ResetBits(GPIOA,GPIO_Pin_6);
GPIO_SetBits(GPIOA,GPIO_Pin_7);
Delay(4600000);Delay(4600000);Delay(4600000);
GPIO_SetBits(GPIOA,GPIO_Pin_5);
GPIO_SetBits(GPIOA,GPIO_Pin_6);
GPIO_ResetBits(GPIOA,GPIO_Pin_7);
Delay(4600000);Delay(4600000);Delay(4600000);
}
}
接线
-
连接st-link与stm32芯片:按照下图对应连接
-
将芯片插在面包插在面包板上
-
插入led二极管:二极管长脚为正级、短脚为负级,所以长脚应该接入电源,短脚与stm32芯片相接
-
利用stm32为面包板供电(也可用面包板的电源模块进行供电,我嫌麻烦,所以就直接用芯片供电了)
程序烧入芯片
-
编译程序
-
配置st-link:在魔法棒中配置
-
烧入程序:点击LOAD图标烧入
结果观察
可以观察到三个led灯依次闪烁,时间间隔约为一秒
2. 汇编语言版
汇编语言我不会,学着太麻烦了,所以参考了另外一位博主的代码,尝试理解了一下,并把它的代码烧入了试试,也是可行的,相当于是验证性实验了,代码写得很详细。
注意:此代码是将PA7,PB9,PC15作为输出的,与上个C语言代码不同
具体工程配置、具体讲解可参考他的博客: STM32F103C8T6实现流水灯(c语言和汇编两个版本)
RCC_APB2ENR EQU 0x40021018;配置RCC寄存器,时钟,0x40021018为时钟地址
GPIOC_CRH EQU 0x40011004;配置GPIOC_CRH寄存器,是端口配置高寄存器,高位的偏移地址为0x04
GPIOC_ORD EQU 0x4001100c;配置GPIOC_ORD寄存器,是端口输出寄存器,输出由这里控制
GPIOA_CRL EQU 0x40010800;配置GPIOC_CRH寄存器,是端口配置高寄存器,高位的偏移地址为0x04
GPIOA_ORD EQU 0x4001080C;配置GPIOC_ORD寄存器,是端口输出寄存器,输出由这里控制
GPIOB_CRH EQU 0x40010C04;配置GPIOC_CRH寄存器,是端口配置高寄存器,高位的偏移地址为0x04
GPIOB_ORD EQU 0x40010C0C;配置GPIOC_ORD寄存器,是端口输出寄存器,输出由这里控制
Stack_Size EQU 0x00000400;栈的大小
;分配一个stack段,该段不初始化,可读写,按8字节对齐。分配一个大小为Stack_Size的存储空间,并使栈顶的地址为__initial_sp。
AREA STACK, NOINIT, READWRITE, ALIGN=3 ;NOINIT: = NO Init,不初始化。READWRITE : 可读,可写。ALIGN =3 : 2^3 对齐,即8字节对齐。
Stack_Mem SPACE Stack_Size
__initial_sp
AREA RESET, DATA, READONLY
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
AREA |.text|, CODE, READONLY
THUMB
REQUIRE8
PRESERVE8
ENTRY
Reset_Handler
bl LED_Init;bl:带链接的跳转指令。当使用该指令跳转时,当前地址(PC)会自动送入LR寄存器
MainLoop BL LED_ON_C
BL Delay
BL LED_OFF_C
BL Delay
BL LED_ON_A
BL Delay
BL LED_OFF_A
BL Delay
BL LED_ON_B
BL Delay
BL LED_OFF_B
BL Delay
B MainLoop;B:无条件跳转。
LED_Init;LED初始化
PUSH {R0,R1, LR};R0,R1,LR中的值放入堆栈
LDR R0,=RCC_APB2ENR;LDR是把地址装载到寄存器中(比如R0)。
ORR R0,R0,#0x1c;ORR 按位或操作,11100将R0的第二位置1,其他位不变
LDR R1,=RCC_APB2ENR
STR R0,[R1];STR是把值存储到寄存器所指的地址中,将r0里存储的值给rcc寄存器
;上面一部分汇编代码是控制时钟的
;初始化GPIOA部分
LDR R0,=GPIOA_CRL
BIC R0,R0,#0x0fffffff;BIC 先把立即数取反,再按位与
LDR R1,=GPIOA_CRL
STR R0,[R1]
;上面的代码是初始化CPIOC_CRH
LDR R0,=GPIOA_CRL
ORR R0,#0x20000000;开启的是pc15,所以是2,为0100,是推挽输出模式,最大速度为2mhz
LDR R1,=GPIOA_CRL
STR R0,[R1]
;GPIOC的端口配置高寄存器配置完毕,也就是CPIOA_CRH配置完成,端口的输出模式确定,不使用的都设为复位后的状态,为0100,所以上面处理输出为都是4
;将PC15置1
MOV R0,#0x80; 二进制为0b1000 0000 ,第7位就是a7引脚的输出电压
LDR R1,=GPIOA_ORD ;由r1控制ord寄存器
STR R0,[R1] ;将ord寄存器的值变为r0的值
;初始化GPIOB部分
LDR R0,=GPIOB_CRH
BIC R0,R0,#0xffffff0f;BIC 先把立即数取反,再按位与,用的是b9,所以把第二位置零
LDR R1,=GPIOB_CRH
STR R0,[R1]
;上面的代码是初始化CPIOC_CRH
LDR R0,=GPIOB_CRH
ORR R0,#0x00000020;开启的是pc15,所以是2,为0100,是推挽输出模式,最大速度为2mhz
LDR R1,=GPIOB_CRH
STR R0,[R1]
;GPIOC的端口配置高寄存器配置完毕,也就是CPIOA_CRH配置完成,端口的输出模式确定,不使用的都设为复位后的状态,为0100,所以上面处理输出为都是4
;将PC15置1
MOV R0,#0x200; 二进制为0b10 0000 0000,第16位就是b9引脚的输出电压
LDR R1,=GPIOB_ORD ;由r1控制ord寄存器
STR R0,[R1] ;将ord寄存器的值变为r0的值
;初始化GPIOC部分
LDR R0,=GPIOC_CRH
BIC R0,R0,#0x0fffffff;BIC 先把立即数取反,再按位与,就是将三十二位全部置零
LDR R1,=GPIOC_CRH
STR R0,[R1]
;上面的代码是初始化CPIOC_CRH
LDR R0,=GPIOC_CRH
ORR R0,#0x20000000;开启的是pc15,所以是2,为0100,是推挽输出模式,最大速度为2mhz
LDR R1,=GPIOC_CRH
STR R0,[R1]
;GPIOC的端口配置高寄存器配置完毕,也就是CPIOA_CRH配置完成,端口的输出模式确定,不使用的都设为复位后的状态,为0100,所以上面处理输出为都是4
;将PC15置1
MOV R0,#0x8000; 二进制为0b1000 0000 0000 0000,第16位就是c15引脚的输出电压
LDR R1,=GPIOC_ORD ;由r1控制ord寄存器
STR R0,[R1] ;将ord寄存器的值变为r0的值
POP {R0,R1,PC};将栈中之前存的R0,R1,LR的值返还给R0,R1,PC
LED_ON_A;亮灯
PUSH {R0,R1, LR}
MOV R0,#0x00 ;二进制为0b0000 0000 0000 0000,第16位为0,后面将作为pc15的输出电压
LDR R1,=GPIOA_ORD ;将GPIOC的地址赋予r1
STR R0,[R1];将r0的值赋予在GPIOC_ORD中
POP {R0,R1,PC}
LED_OFF_A;熄灯
PUSH {R0,R1, LR}
MOV R0,#0x80 ;二进制为0b 1000 0000 0000 0000,第16位为1,后面将作为pc15的输出电压
LDR R1,=GPIOA_ORD ;将GPIOC的地址赋予r1
STR R0,[R1] ;[]是指对里面的地址操作,所以是将r0的值赋予GPIOC_ORD
POP {R0,R1,PC}
LED_ON_B;亮灯
PUSH {R0,R1, LR}
MOV R0,#0x000 ;二进制为0b0000 0000 0000 0000,第16位为0,后面将作为pc15的输出电压
LDR R1,=GPIOB_ORD ;将GPIOC的地址赋予r1
STR R0,[R1];将r0的值赋予在GPIOC_ORD中
POP {R0,R1,PC}
LED_OFF_B;熄灯
PUSH {R0,R1, LR}
MOV R0,#0x200 ;二进制为0b 1000 0000 0000 0000,第16位为1,后面将作为pc15的输出电压
LDR R1,=GPIOB_ORD ;将GPIOC的地址赋予r1
STR R0,[R1] ;[]是指对里面的地址操作,所以是将r0的值赋予GPIOC_ORD
POP {R0,R1,PC}
LED_ON_C;亮灯
PUSH {R0,R1, LR}
MOV R0,#0x0000 ;二进制为0b0000 0000 0000 0000,第16位为0,后面将作为pc15的输出电压
LDR R1,=GPIOC_ORD ;将GPIOC的地址赋予r1
STR R0,[R1];将r0的值赋予在GPIOC_ORD中
POP {R0,R1,PC}
LED_OFF_C;熄灯
PUSH {R0,R1, LR}
MOV R0,#0x8000 ;二进制为0b 1000 0000 0000 0000,第16位为1,后面将作为pc15的输出电压
LDR R1,=GPIOC_ORD ;将GPIOC的地址赋予r1
STR R0,[R1] ;[]是指对里面的地址操作,所以是将r0的值赋予GPIOC_ORD
POP {R0,R1,PC}
Delay
PUSH {R0,R1, LR}
MOVS R0,#0
MOVS R1,#0
MOVS R2,#0
DelayLoop0
ADDS R0,R0,#1
CMP R0,#330
BCC DelayLoop0
MOVS R0,#0
ADDS R1,R1,#1
CMP R1,#330
BCC DelayLoop0 ;无进位
MOVS R0,#0
MOVS R1,#0
ADDS R2,R2,#1
CMP R2,#15
BCC DelayLoop0
POP {R0,R1,PC}
NOP
END
结果观察
三、总结
本次实验用C语言和汇编语言的方式实现了,主要是练习了如何翻看芯片手册来获取想要的信息,也从一定程度上掌握了程序的烧入、调试方法,这也是自己的第一次硬件尝试,终于是迈入了第一只脚进去,以前一直对这个东西有种恐惧,做完了之后也发现没有那么难,也产生了点兴趣。
虽然确实学的过程有点煎熬,以后慢慢来吧。
资料下载连接(含汇编语言、C语言流水灯工程,芯片资料手册、数据手册):
链接:https://pan.baidu.com/s/1IWem14AVSB9_i4AyY7VNdQ
提取码:z6kw
四、参考文献
【嵌入式系统】存储器 映射与寄存器映射原理
STM32寄存器的简介、地址查找,与直接操作寄存器
STM32入门-GPIO初始化步骤
STM32F103C8T6实现流水灯(c语言和汇编两个版本)