如果不会寄存器开发而陷入瓶颈, 那么本文将会有较大帮助

如果不会寄存器开发而陷入瓶颈,那么本文将会有较大帮助

38a628572213814de4448c618009474d.png///插播一条:我自己在今年年初录制了一套还比较系统的入门单片机教程,想要的同学找我拿就行了免費的,私信我就可以哦~点我头像黑色字体加我地球呺也能领取哦。最近比较闲,带做毕设,带学生参加省级或以上比赛///9de07e0d1520ae62443fb97a5e64a501.png

0.绪论

对于经过系统培训的开发者.单片机或SoC的驱动开发,不管是使用各种库还是直接上寄存器,都不成问题.

可是很多非专业但是需要干嵌入式或单片机工程的人,比如机械,车辆工程,物理,或是其他的一些专业.有时候这些学生需要搞比赛,做项目,不可避免的要用到一些单片机(由于种种原因,现在几乎都会首选STM32).但是缺乏系统训练的学生往往无法看懂寄存器版本的例程,或是别人的开源项目.自己写的话更是不知道如何开始.或者编译完成后总是出现莫名的问题.这给大家带来了极大的困难.

同时互联网上大部分教程都是转载来转载去,各种差异和版本都有.很多人即使看过了也是一头雾水.所以应一个同学要求,大概写一下寄存器的一些操作.

本教程将会使用"学生用户体量庞大"的STM32F1xx系列的单片机作为例子.但是不必担心.所有的寄存器操作都是共通的.并不会有本质的差异.希望你能快速阅读完并且理解完,然后查阅自己需要的资料.不管是什么东西,这种方式都是通用的.

嵌入式软件开发具有一个比较庞大的知识体系.限于作者的时间和水平,本文不可能讲太多东西.但是如果你只是因为不会寄存器开发而陷入瓶颈,那么本文将会有较大帮助(毕竟这个是写文章的目的).

理解本文需要一些基础,大概包括: c/c++,一些数学,计算机基础知识.大多数理工类专业都会有相关的课程的.如果确实存疑可以即时搜索.

让我们先记住开发的正确方式: 摘抄借鉴,修改糅合,以实现功能为目的,以别人的代码为基础.

只要不涉及什么知识产权的话,这样做是完全没有问题的.毕竟不需要把很多基础性的东西写来写去.所以请大胆的找开源项目,尽可能在基础上改,而不是从零开发.不废话了,开始正事.

比如十进制下的0.1就是二进制的无限不循环小数.上面那个例子也是.

著名的 IEEE754 float浮点数标准导致的bug:在很多语言中, 0.1+0.2≠0.3

2af71fc1d3cdd9759efc7c1e5ffafbe4.png

就是因为0.1是二进制无限循环小数的原因.但是存储器位宽不能是无穷的.所以产生舍入误差.

7e07aed7e8ad2b986292f5d09600f34a.png在浏览器中按下F12进入开发者模式,尝试JavaScript下的浮点数精度bug

所以在很多项目中,为了实现当两个值相等时触发什么函数,往往不会直接写相等,而是两者的差值小于多少时即生效

floata,b;........if(a==b){

//这种写法不建议}........if(abs(a-b)0.00001){

//一般这么写}

二进制与十/十六进制的转换

刚才那个方法就可以直接算.还有其他算法这里不讲.先让我贴一个表格:

BIN

DEC

HEX

0000

0

0

0001

1

1

0010

2

2

0011

3

3

0100

4

4

0101

5

5

0110

6

6

0111

7

7

1000

8

8

1001

9

9

1010

10

A

1011

11

B

1100

12

C

1101

13

D

1110

14

E

1111

15

F

BIN是二进制, DEC是十进制, HEX是十六进制.都是英语简写.

十六进制和二进制可以直接换算.方法是每四位二进制看作一个十六进制

比如

0110 1101 1011 1001

6 D B 9

转换表背过的话读代码快些.因为一般情况下,为了让一行代码看的不至于太长,人们会用十六进制代表二进制.尤其是对于拥有32位cortex-M3内核的STM32F1系列单片机,一次写一个三十二位数属实太冗长.

c/c++中,默认写的数字都是十进制.二进制应该是0b开头,比如0b00101100,而十六进制是0x开头,比如0x3C.

//一般这么写GPIOB->CRL&=0x00440000;//这么写就不太美观了GPIOB->CRL&=0b00000000010001000000000000000000;

数学差不多了.开始正文.

2. c/c++语言基础

a+b; //加法

a-b; //减法

a*b; //乘法

a/b; //除法,所有计算需要注意整型(整数)和浮点型(小数)的运算区别.如有疑问自行搜索

a%b; //求模.就是小学学的余数. 14÷4=3 ... 2 这里 14mod4=2, 14/4=3. float(14)/4=3.5f

a</左移.将a看为二进制数,整个向左移动b位.比如&nbsp;00000110<就是&nbsp;00011000.当溢出的时候会发生什么呢?

a>>b; //右移.和上面一样的功能.

a&b; //按位求与(AND),比如&nbsp;&nbsp;&nbsp;11010010

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// & 01010011

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// ------------

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// 01010010

a|b; //按位取或(OR)

~a; //按位取非(NOT)

!a; //逻辑非(注意和上面的区别)

c+=a; //相当于&nbsp;c=c+a;其他算符同理.

重点看左右移和位操作(与,或,非等).

3.我们配寄存器(register)到底是在配置什么

首先,你的最终目的都是使用单片机的GPIO(general pin input & output)读取/输出一个高电平还是低电平.不管是诸如I2C, SPI的通信,还是按键读取,亮灯报警,说到底都是高低电平的控制或探测.

ADC输入的是模拟(Analog)信号,但是会被转化为数字(Digital)信号,一样是高低电平.这里暂且不谈
外设,时钟,或者一些功能的使能及配置一样是通过寄存器的.原理相似.毕竟芯片集成电路也是电路,而且是数字电路.暂时不深入.

所以,为了让某一个Pin输出电平,或者使能一个通道,我们可以用0或者1实现.

但是代码终归是代码,不是魔法.为了使需求生效,单片机将每一个需要控制的量,赋予一个地址.在电路层面上实现相关的功能绑定.用户只需通过给这个地址去写一个值,就相当于控制了需要控制的东西.易于计算,我们的32位处理器最大可以寻址4GB的内存空间.

注意:并不是每一个地址都是有真实物理地址对应的.换而言之,一个地址可能指向的是真实的内存,也可能并不是真实存在的内存.不过访问这个地址相当于控制了被控量一样.此时该地址可以看作控制量的一个句柄(handler).
"使能"的意思是enable.反义词是"禁用(disable)".计算机相关的词汇总是这么的看不懂字面意思.看英语就明白了.
内存是存储数据的地方.任何电子信息数据都可以看做01串. 32位机的最小存储单元是DWORD(双字, Double Word),包含32比特位. 32位机的内存单元可以看作一个个存储32比特位的小仓库.为了找到所需要的数据,需要给这些仓库编号.这个所谓的编号就是地址(Address).存储的值是地址值的变量叫做指针(pointer).比如一个仓库放着一个记录某个货物的多个存放仓库的编号,那么这个内存里的数据就是一个指针.计算机无法分辨哪写是指针,哪些是数据.这需要人去完成.

库函数是寄存器的封装.不管是ST出的HAL硬件抽象层库,还是标准库STL,都是封装而已.本质上是宏定义替换和一些函数.宏定义在编译(compile)阶段完成.不占用宝贵的存储空间.函数本身会被编译为代码,所以会占用空间.尽量避免在库中使用函数.当然用户代码肯定无所谓.

为什么要用库函数呢?比如

GPIO_InitTypeDef GPIO_Initure;

GPIO_Initure.Pin=GPIO_PIN_13;

GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP;

HAL_GPIO_Init(GPIOB,&GPIO_Initure);

HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_SET);

这一段代码,不加注释你也知道他的意思是初始化PB13为推挽输出.

但是如果写成了寄存器版本的你可能就看不懂了.

GPIOB->CRH&=0XFF0FFFFF;

GPIOB->CRH|=0X00300000;

GPIOB->ODR|=1<

现在会简要介绍寄存器的配置方法.你能搜到这个文章肯定是因为你找见的开源代码使用的是寄存器版本.&nbsp;正常人的话,能找见库函数绝对是不会用寄存器版本的代码的.

寄存器版本的代码实际上也是封装,比如上述代码实际上也可以直接写成

(uint32_t *)(((((uint32_t)0x40000000) + 0x10000) + 0x0C00)+0x01)&=0XFF0FFFFF;

(uint32_t *)(((((uint32_t)0x40000000) + 0x10000) + 0x0C00)+0x01)|=0X00300000;

(uint32_t *)(((((uint32_t)0x40000000) + 0x10000) + 0x0C00)+0x04)|=1<

//这一段代码可能不对

上面就是直接通过指针操作寄存器的方式.寄存器宏定义和结构体相当于,先找到GPIOB的开头地址,然后加上结构体的附加地址偏移(去操作某一个功能,比如CRH寄存器).最后给这个地址写入一串二进制值去控制相应的东西.

说到底还是去给某些比特上写高低电平.

类似于GPIOB->CRH这种,实际上是定义的一个结构体.

typedefstruct{

__IOuint32_tCRL;

__IOuint32_tCRH;

__IOuint32_tIDR;

__IOuint32_tODR;

__IOuint32_tBSRR;

__IOuint32_tBRR;

__IOuint32_tLCKR;}GPIO_TypeDef;

通过结构体的位关系以及各种宏定义替换,可以实现地址的编译时自动确定.

寄存器的封装好在对于实际内存位置不同的芯片,移植只需更改寄存器宏定义映射即可.而且已经很容易读了.

4.常见的寄存器配置初始化(使用STM32F1xx系列举例子)

我们写驱动,最常用的就是GPIO的配置了.我们就举一些例子.

RCC寄存器

RCC寄存器一般用来配置RCC时钟相关的代码.比如我要使用PA0的话,我必须开启PA的时钟通道才行.时钟相当于处理器的心跳,没有心跳当然是死的.但是没理由的开启时钟会带来额外的EMI干扰和整机功耗.原理暂且不谈,实际危害就是干扰大了,耗电多了.尤其针对于一些对功耗要求严格的场合,例如手环手表,应该尽可能在待机时关闭能关闭的所有时钟.用来省电(比如有的智能手表升级系统后待机变久了,可能就是这个原因.我们说的固件(firmware)升级实际上指的就是把新写的代码下载到了老设备上).

举个例子, RCC时钟的配置如下

RCC->APB2ENR|=1</使能PORTC时钟

RCC->APB2ENR|=1</使能PORTB时钟

RCC->APB2ENR|=1</使能PORTA时钟

具体使能哪一个位是控制谁,需要查表(除非你背过了).

如果是需要快速出项目,不管其他的,可以省事,直接使能所有时钟.但是不建议这么干.

猜你喜欢

转载自blog.csdn.net/l16756062003/article/details/124751555