寄存器开发
- 1、单片机的简介
- 1.1、什么是单片机
- 1.2、F1系列内核和芯片的系统架构
- 1.3、存储器映像
- 1.4、什么是寄存器
- 2、寄存器开发模板工程
- 3、使用寄存器点亮一个LED
- 4、代码改进1
- 5、代码改进2
本教程使用的是STM32F103C8T6最小系统板,教程来源B站up“嵌入式那些事”。
1、单片机的简介
1.1、什么是单片机
单片机(Single-Chip Microcomputer)单片机微型计算机,是一种集成电路芯片,把具有数据处理能力的中央处理器CPU、随机存储器RAM、闪存flash、多种l/O口和中断系统、定时器/计数器等功能(可能还包括显示驱动电路、脉宽调制电路、模拟多路转换器、A/D转换器等电路)集成到一块硅片上构成的一个小而完善的微型计算机系统,在工业控制领域广泛应用。
STM32F103C8T6又被称为32位单片机,那么这个32代表着什么意思喃?
——其中32代表着地址总线有32位,即最小存储单元(1字节B)的地址编号是32位二进制构成的。所以stm32的存储空间位2^32B = 4GB(地址:0x0000 0000~0xFFFF FFFF)
①FLASH:FLAS又称闪存,它是ROM的一部分(只读存储器,现在往里面写数据),用于存放向单片机烧录的代码。而ROM的另外一部分为系统存储器和字节选项。一般情况下单片机读取FLASH里面的数据要通过FLASH接口,所以读取FLASH的数据比较慢。
②RAM:临时存储器,掉电里面的数据会丢失,读写速度比ROM快。主要存储代码运行时的临时变量,内核和片上外设寄存器的配置参数。
③总线:就像是桥梁,用于CPU与外设之间的连接,DMA与外设之间的连接等,进行数据从传输
④时钟:由内部晶振或者外部晶振产生的时钟频率脉冲信号,时钟频率决定了CPU处理0/1的快慢程度(单片机执行代码的快慢程度),除了CPU需要时钟频率脉冲信号,片上外设也需要时钟信号时钟频率脉冲信号,其作用是启动边沿触发器。而像定时器这样的片上外设需要时钟频率脉冲信号来进行计数。
⑤外设:单片机除了CPU还有很多的片上外设,比如GPIO,定时器TIM等,只有CPU和这些片上外设共同作用才能完成我们想要的功能
1.2、F1系列内核和芯片的系统架构
如上图为F1系列单片机内核和芯片的系统架构,其中分为4个主动单元和4个被动单元。
-
四个主动单元
——能够主动的发起请求,主动的访问数据。
①Dcode总线,②System总线,③DMA1,④DMA2(stm32f103c8t6没有DMA2) -
四个被动单元
——不能够主动的发起请求,被动的访问数据。
①FLASH,②SRAM,③FSMC,④桥接1和桥接2上面连接的片上外设
1.3、存储器映像
由前面得知stm32单片机里面有4GB的存储空间,人们将这4GB的空间进行了地址编号就叫存储器映像。划分为8个区域,不同区域代表存储不同功能的信息。如下图所示:
由上图存储器映像得出如下结论:
1.4、什么是寄存器
寄存器的本质就是内存,若通过给某个内存a里面写入数据来控制外设A,那么这个内存a就是外设A的控制寄存器。若某个内存b里面的数据来表示此时外设B的状态,那么这个内存b就是外设B的状态寄存器。我们知晓stm32里面由很多的外设,那么这些外设的寄存器怎么寻找喃?答案:通过地址,我们由前面得知外设寄存器的起始地址为0x4000 0000,即之后的地址就代表着寄存器地址,不同的寄存器的地址不同。 如下图为部分外设的寄存器地址
而寄存器开发就是找到外设的寄存器,通过给这些寄存器里面写入数据来进行对外设配置,进而完成我们想要实现的功能,一般情况下单片机使用寄存器开发的执行效率比使用库函数开发的执行效率更高,但是人们的寄存器代码开发效率会比使用库函数的代码开发效率低。
2、寄存器开发模板工程
开发环境安装和创建工程请参考stm32标准库入门教程的第1章和第2章。寄存器开发必要文件如下图所示:
模板资料链接: link
3、使用寄存器点亮一个LED
实物按照如下图所示连接好。创建好工程模板后,我们在模板中的main()函数里面按照如下步骤编写代码:
1、开启时钟
如图LED引脚连接着A0,则开启片上外设GPIOA的时钟——RCC寄存器(与外设的时钟有关)
① 打开参考手册找到存储器映像如下图所示:
如图所示:地址0x4002 1000——0x4002 13FF这段的内存是与RCC有关的寄存器。
②打开参考手册找到RCC寄存器描述如下图所示:
如图RCC的第一个寄存器为RCC_CR(时钟控制寄存器),它的地址偏移为0x00,则这个寄存器的地址就是0x4002 1000。RCC的第二个寄存器为RCC_CFGR(时钟配置寄存器),它的地址偏移为0x04,那为什么第二个寄存器的地址偏移是0x04喃?
如图:寄存器的地址其实是所包含4个字节中的首字节的地址,一个寄存器中包含4个字节,所以每增加一个寄存器,地址就偏移0x04。
③GPIOA挂载在APPB2时钟总线上面,我们找到APB2使能寄存器如下图所示:
如图RCC_APB2ENR寄存器的地址偏移为0x18,而与GPIOA时钟的位在32位中的第3位,只需要在这一位写入1,即开启了片上外设GPIOA的时钟。那如何用代码表示喃?
*(uint32_t *)(0x40021000 + 0x18) = 0x04;//指针代表地址
将十六进制的数值通过类型转换为指针类型,然后通过*取内容给其赋值。
2、配置IO口的输出模式
①打开参考手册找到存储器映像如下图所示:
如图:地址0x4001 0800——0x4001 0BFF这段的内存是与GPIOA引脚有关的寄存器。
②打开参考手册找到GPIO寄存器描述如下图所示:
如上图:配置低引脚IO(IO0~IO7)的寄存器的地址偏移为0x00,那么配置高引脚IO(IO8 ~ IO15)的寄存器的地址偏移为0x04。而我们的LED负极连接着A0,所以我们需要对低寄存器进行配置。那如何用代码配置喃?
*(uint32_t *)(0x40010800 + 0x00) = 0x03;//MODE0 = 11,CNF0 = 00
MODE0 = 11,表示配置为输出模式,且输出速度为50MHz
CNF0 = 00,表示配置为通用推挽输出模式(能输出低电平0和高电平1)
地址为0x4001 0800如何进行地址偏移,若需要配置GPIOB0则地址为0x4001 0C00进行地址偏移
3、IO输出逻辑电平
①打开参考手册找到GPIO寄存器描述如下图所示:
如图:给ODR0写入1则对应的IO0引脚则输出高电平1,若写入0,则引脚输出低电平0
由实物连接图可知,LED的正极连接着VCC,负极连接着IO引脚,所以引脚输出低电平,LED点亮,输出高电平,LED熄灭。代码如下:
*(uint32_t *)(0x40010800 + 0x0C) = 0xFFFE;//1111 1111 1111 1110
IO1引脚~IO15引脚输出高电平,IO0引脚输出低电平
综上:使用寄存器编程点亮一个LED灯完整的代码如下:
int main(void)
{
//1、开启对应的GPIOA的时钟
*(uint32_t *)(0x40021000 + 0x18) = 0x04;
//2、给IO口设置工作模式:PA0配置为通用推挽输出模式
*(uint32_t *)(0x40010800 + 0x00) = 0x03;
//3、对应的IO口设置值:1/0,PA0输出0,点亮LED
*(uint32_t *)(0x40010800 + 0x0C) = 0xFFFE;
while(1)
{
}
}
4、代码改进1
从上面的代码开发步骤中,我们每次需要配置某个片上外设的寄存器时,我们都需要打开存储器映像来找到这个片上外设的寄存器初始地址(基地址),然后在通过寄存器的地址偏移,然后通过基地址+偏移地址最终定位到我们需要的那个寄存器的地址。使用这种开发方式大大降低了我们开发的效率,而st公司也想到了这一点。
所示它在stm32f10x.h这个文件里面已经将每个片上外设的寄存器基地址用#define定义好了,如下图所示:RCC的基地址就用RCC这个字符串来表示了(即RCC = (uint32_t *)(0x40021000))
我们以外设RCC为例,查看它的代码是如何定义的,如下图所示:
如图:RCC的基地址 =RCC_BASE = 0x4000 0000 + 0x20000 +0x1000 = 0x4002 1000,正好和存储器映像里面的一样。
而RCC_TypeDef *(RCC_BASE)将基地址转换为结构体的指针类型,我们转到这个结构体的定义看看,如下图所示:
如上图所示:这个结构体里面的变量全是RCC的寄存器,且都是按照参考手册的顺序一比一排列的,且都是定义的和参考手册一样的32位(方便地址偏移)。则由C语言结构体的如下的基础知识
P->a :结构体的基地址偏移了1个字节,P->b:结构体的基地址偏移了5个字节。
既然RCC就是结构体的地址,那么通过地址表示结构体的变量如下:
RCC->CR:RCC寄存器的基地址偏移了0位
RCC->CFGR:RCC寄存器的绝对值偏移了32位
RCC->CIR:RCC寄存器的绝对值偏移了64位…
综上:通过上面的基础知识,我们进行如下的代码改进
int main(void)
{
//1、开启对应的GPIOA的时钟
//*(uint32_t *)(0x40021000 + 0x18) = 0x04;
//2、给IO口设置工作模式:PA0配置为通用推挽输出模式
//*(uint32_t *)(0x40010800 + 0x00) = 0x03;
//3、对应的IO口设置值:1/0,PA0输出0,点亮LED
//*(uint32_t *)(0x40010800 + 0x0C) = 0xFFFE;
/*
改进1:地址换为st公司定义好的宏
*/
//1、开启对应的GPIOA的时钟
RCC->APB2ENR = 0x04;
//2、给IO口设置工作模式:PA0配置为通用推挽输出模式
GPIOA->CRL = 0x03;
//3、对应的IO口设置值:1/0,PA0输出0,点亮LED
GPIOA->ODR = 0xFFFE;//1111 1111 1111 1110
while(1)
{
}
}
5、代码改进2
1、为了提高程序员开发的效率,st公司早已经将需要写入寄存器里面的值使用宏定义好了。
例如:开启对应的GPIOA的时钟,我们需要给RCC_APB2ENR寄存器里面写入数值0x04,而我们则可以通过RCC_APB2ENR_IOPAEN这段字符串来代替0x40这个数值。
RCC->APB2ENR = 0x04;//开启对应的GPIOA的时钟
改进为:
RCC->APB2ENR = RCC_APB2ENR_IOPAEN; //开启对应的GPIOA的时钟
2、前面给寄存器写入数据的时候,都是通过某某寄存器 = 数值。这样写入数据必然会影响寄存器中的其他位置的数据,例如GPIOA->ODR = 0xFFFE;//1111 1111 1111 1110这段代码,我只需要个IO0口输出低电平0,虽然我们成功写入了0,但是其他的IO引脚却因为我们写入的数据写入了高电平1。为了解决这种现象,我们可以通过位运算<<, >>, |, &, ~来解决我们的问题。
RCC->APB2ENR = RCC_APB2ENR_IOPAEN; //开启对应的GPIOA的时钟
改进为:
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; //开启对应的GPIOA的时钟
综上:改进的代码如下:
int main(void)
{
/*
改进2:地址换为st公司定义好的宏,且使用位运算
*/
//1、开启对应的GPIOA的时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
//2、给IO口设置工作模式:PA0配置为通用推挽输出模式,即MODE0 = 11,CNF0 = 00
GPIOA->CRL |= GPIO_CRL_MODE0;
GPIOA->CRL &= ~GPIO_CRL_CNF0;
//3、对应的IO口设置值:1/0,PA0输出0,点亮LED
GPIOA->ODR &= ~GPIO_ODR_ODR0;//PA0引脚输出0
while(1)
{
}
}
改进代码总结:
①寄存器使用结构体指针来进行表示
②写入寄存器的值使用通过定义好的宏
③为了不影响寄存器的其他位的值最好使用位运算
④位运算时:若需要写入寄存器的值为1则用 |=,若需要写入寄存器的值为0则用 &=(其中数值则需要取反)。