观前提醒:只是我学单片机的一个简单记录,会比较啰嗦,但是我在研究单片机寄存器过程中真实的心得体会,希望能帮到读者。
打完电赛觉得自己的单片机白学了,于是想从寄存器从头开始学一遍单片机,刚好前段时间发现合宙AIR001这款单片机,外设较少且相对简单,就买了一块来学习寄存器的编程。
合宙AIR001是一款国产的单片机,而且资料基本全是中文的,对初学者非常友好,合宙官方整理的资料非常优秀,但例程只有HAL库和LL库,不过刚好适合想学寄存器的uu。
单片机只要10块钱啊,加调试器也只是20块啊,重点是还包邮,3杯蜜桃四季春加个甜筒的价格,买不了吃亏买不了上当,xdm快冲(一个64脚的f103芯片都要10多块了)
根据教程搭好环境后,官方给的LED闪烁例程如下,是HAL库的,想着用寄存器代码复现一遍
养成好习惯,用啥先看手册
根据之前学的一些皮毛,知道大致流程是配置时钟→初始化输出管脚→操作管脚实现相应功能
所以先找到了时钟使能寄存器
看原理图三个板载LED都是GPIOB的
所以要这样写寄存器的代码
RCC->IOPENR |= 1<<1;
为啥要这样写呢?
说的形象一点,这样写就能让单片机找到GPIOB时钟的开关,然后拨上去。
具体是怎么找到的呢?
首先是RCC,RCC是一个宏定义,代表的是RCC类寄存器的起始地址
在手册第三章就有,把上面三个加起来就是下面这张表里面RCC的起始地址了
搜了一下,UL是unsigned long类型的意思,我也不是太懂为什么要在后面加个UL
通过RCC这个宏定义,我们找到了RCC类寄存器的起始地址,但在这个类里面有很多个寄存器,我们要的是使能GPIOB的时钟,所以用到了IOPENR这个寄存器
RCC->IOPENR |= 1<<1;
结构体->结构体成员
这样我们就能让单片机找到IOPENR寄存器的地址了
需要注意的是这个RCC其实是一个强制类型转化
这一部分其实指的是RCC类寄存器IOPENR寄存器的内容
*addr就是指addr这个地址里面存储的内容嘛,只不过这里的addr比较长罢了。。
RCC->IOPENR
((RCC_TypeDef *) RCC_BASE)->IOPENR
((RCC_TypeDef *) (AHBPERIPH_BASE + 0x00001000UL))->IOPENR
((RCC_TypeDef *) ((PERIPH_BASE + 0x00020000UL) + 0x00001000UL))->IOPENR
((RCC_TypeDef *) (((0x40000000UL) + 0x00020000UL) + 0x00001000UL))->IOPENR
这里单片机已经能找到GPIOB时钟所在的区域了,但看手册发现这个区域有三个开关,我们要拨动GPIOB的开关其实就把GPIOBEN这一个开关置为1,就能让GPIOB的时钟使能了
这里有很多种写法
可以是
RCC -> IOPENR = 0x2;
想遵守一下编程规范可以写成这样(32位的数字嘛)
RCC -> IOPENR = 0x00000002;
但以上两种可能会影响其他时钟
比如已经打开了GPIOA的时钟,这里如果这样写就会把GPIOA的时钟关掉
如果想单独操作一位可以这样
RCC->IOPENR |= 1<<1;
(1<<1)代表把1向左移一位
0000 0000 0000 0000 0000 0000 0000 0001
↓
0000 0000 0000 0000 0000 0000 0000 0010
用上面这个数去和下面这个寄存器里的数据做位或运算(位运算是啥自己去搜吧)
0000 0000 0000 0000 0000 0000 0010 0001(代表GPIOA和GPIOF的时钟已经打开了)
0000 0000 0000 0000 0000 0000 0000 0010
----------------------------------------与运算
0000 0000 0000 0000 0000 0000 0010 0011(经过与运算后寄存器里的数据)
最后寄存器里的数据就只有第二位的数据从0变成1
如果要置为0的话就和 ~(0<<n) 做与运算
到这里应该就能明白这一行代码为什么这样写了
RCC->IOPENR |= 1<<1;
理解了以上内容后我们就能结合开发手册对任意寄存器任意一位进行直接操作了
这里放上主函数的代码,接下来从管脚初始化来逐行讲解
#include "air001xx_hal.h"
#include "air001_dev.h"
int main(void)
{
HAL_Init();
//HAL库初始化,因为要用到delay函数,但我还不会用寄存器写delay函数
//所以用HAL库的delay函数
//时钟使能
RCC->IOPENR |= 1<<1;
//PB3管脚初始化
GPIOB->MODER &= (0x01)<<6;
//LED循环闪烁
while(1)
{
GPIOB->BSRR |= 1<<3;
HAL_Delay(500);
GPIOB->BSRR |= 1<<19;
HAL_Delay(500);
}
}
先对PB3进行端口模式初始化,结合手册,如果要配置PB3的端口模式
GPIOB->MODER ^= 1<<7;
这里有个坑,寄存器里有个Reset value,我们只需要把MODE3对应的两位寄存器改成01,但或运算不能完成这一操作了,所以这里我使用了异或运算。简单粗暴一点就是直接赋对应值,但那样可读性会差很多(本来寄存器代码可读性就很差了)
(怎么说呢,我也没找到位运算有什么统一的使用标准,我觉得只要能实现目标就可以了,这里不一定必须用异或,用其他位运算应该也可以)
总之最后能实现在不影响寄存器其他值的情况下完成对PB3模式的修改。
配置好输出模式后,就可以开始闪灯啦
这里用到BSRR寄存器,看手册的话可以知道BSRR高位(31-16)是用来置0的,低位(15-0)是用来置1的,这里其实也可以用ODR寄存器,但BSRR用来做闪烁似乎……更正统?
其实BSRR本质上也是修改ODR寄存器。
看很多都说用BSRR比ODR好,试了ODR也能实现功能,那就无所谓了。
在BSRR寄存器中对应位分别置1,调试时ODR有状态改变,就实现了闪烁。
(这里delay函数用的HAL库,还不会写寄存器的delay函数)
while(1)
{
GPIOB->BSRR |= 1<<3;//BSRR低位-亮灯
HAL_Delay(500);
GPIOB->BSRR |= 1<<19;//BSRR高位-灭灯
HAL_Delay(500);
}
到这里就实现了通过寄存器点灯,记录一下过程,希望自己以后能记得更新。
以上用到的所有的资料都能在🛴 Air001 - LuatOS 文档这里面找到。
配置好环境后在main.c里复制一遍代码直接烧录就能看到现象了
PS:
打过电赛后,感觉我虽然看上去似乎学了单片机,各种外设如GPIO、ADC、DMA、DAC、UART等都有用过,但解决实际问题时发现自己还处在一个没入门的状态。
究其根本,还是缺乏对寄存器的理解,以至于在使用时只能去抄各种例程。
只能虚浮于应用而不理解原理的感觉让人非常难受,尤其是在需要实现一些特定功能时,就算有例程也不知从何改起,只能去找别人实现功能的代码。
要从根本改善这一情况还是要从寄存器学起,但寄存器显然不是一般人能学会的,尤其时在学了标准库后,根本不愿意去研究寄存器了。
我的老师教单片机时讲的是stm32f103的标准库,搞得我之前很长一段时间都觉得自己很牛,但在涉及原理时啥都不会,如果老师在教单片机时是从寄存器开始教的,请务必认真听。