单片机?无非操作寄存器罢了
1. 一切动作,无非操作寄存器
操作 SFR(特殊功能寄存器)就能让 IO 口拉高拉低。 操作 SFR 就能把中断总闸合上。 操作 SFR 就能让定时器开始哒哒哒地数数。 把 IO 引脚的手柄一高一低地掰出节奏,那就是 IIC,就是 SPI,就是 PWM。 ……
什么串口、什么 I2C、什么 SPI、什么 PWM 输出,拆穿了就是照着协议,在正确的时间点去写那几个寄存器、读那几个寄存器。
举个最俗的例子:串口。 忽略掉所有细节,你看: 把 SCON 配好模式,把定时器 1 的 TH1、TL1 装好初值,波特率就定下来了; 再往 PCON 里把 SMOD 那一位拨一下,波特率还能翻倍; 然后往 SBUF 这个寄存器里扔一个字节,硬件自己就按约定的节拍,把起始位、数据位、校验位、停止位一帧一帧颠出去。 另一边,从 SBUF 里读一个字节,电脑发过来的数据就拿到了。 “通过设置几个寄存器的值,不就设置好了?” 对,就是这个道理。
但是,“细节完全不能忽略”,我们学的,恰恰就是怎么去翻手册找到这些寄存器,搞清楚协议是什么样子,以及把这些寄存器在正确的时间点摆弄成什么值。
2. 你写的 C,怎么变成芯片里的动作(以51为例)
2.1 写 C
你想点个灯:
P0_1 = 1; // 实际操作的是 P0 寄存器的第1位
你眼里是一个变量赋值。这行 C,经过下面的流水线,最终就会变成驱动一颗 LED 亮起的物理电流。
2.2 编译、汇编、链接 → .hex
Keil C51 或者 SDCC 这些编译器,帮你把 C 变成了汇编,再把汇编变成机器码,链接成 .hex 文件。 不知道里面发生了什么,不知道就不知道呗,又不影响写 C。没错,写应用层的时候可以这么任性,为什么单片机现在入门如此简单,就是因为有了c,有编译器帮你去做这些复杂的操作。 但如果哪天你被一个硬件 fault 卡住,想抠那几微秒的时序,就会发现,这个黑箱里的每一步都是你的救命稻草。
2.3 烧录
.hex 文件里是一条一条的机器指令,被烧录器逐字节写进 Flash。
flash
地址 指令 真实存储
0x0000 [jump 0x0080] 0001 0080 <-假设
0x0004 [mov xx, xx] 0002 xxxx <-假设
0x0008 [ ] <-假设
... ....
2.4 上电执行
一上电,芯片内部的硬件逻辑“咔嗒”一下,把 PC(程序计数器)归零。 PC=0,CPU 从 Flash 的 0 地址拿出第一条指令,塞进译码器,译码器认出来这是什么活,执行完毕,PC 自动指向下一条指令的地址(51 指令不定长,有 1 字节、2 字节、3 字节,PC 有时候+1有时候+2,或者+3)。 就这么一条一条地从 Flash 里取指令,一条一条地执行,直到断电。
3. 这不巧了吗?计算机组成原理那本书全活了
上电后,PC=0,从 Flash 地址 0 处取第一条指令(针对51,复位向量就在 0x0000)。 指令五花八门: 有操作 RAM 的,把数据搬来搬去; 有操作 SFR 的,把 IO 口拉高拉低,把中断闸门拉开合上; 有跳转的,把 PC 直接踹到别的地址; 还有空转的 NOP,纯粹耗掉一个机器周期。 这不就是你前面那句话——“无非是操作寄存器”吗?
那 CPU 又是个什么玩意?为什么能看懂指令?为什么能算加法? 大学教过啊:译码器把一大串 0/1 翻译成“哦,这是要把某个寄存器的第几位拉高”;ALU 在那吭哧吭哧做加减乘除与或非。
CPU 怎么从 Flash 取指令?怎么操作 RAM?怎么操作 SFR? Flash 是一栋房子,RAM 是另一栋房子,SFR 又是一排控制柜,CPU 自个儿是个总调度室。 房子之间怎么通信?拉一根电话线嘛——这就是总线。 把地址线、数据线、控制线捆在一起,CPU 往地址线上放门牌号,数据线上就传来对应的内容。 单片机这个小区里房子可多了:I2C、SPI 这些是出小区的省道,通向板子上的传感器、存储器; 小区内部的电话线也有讲究,有些用铜线就够(低速总线 APB),有些得上光纤(高速总线 AHB)。高低速总线就分出来了。
那数据传输怎么搞? 让 CPU 从 A 房子搬数据,再搬到 B 房子?行,但 CPU 被这种纯体力活占着太浪费。 于是引出一个专门干这种活儿的搬运工——DMA。CPU 交代一句:“把 UART 接收到的这 256 个字节搬到 RAM 里”,DMA 就闷头搬完,不劳 CPU 再费神。
什么 RAM?SRAM、DRAM。什么 Flash?NOR Flash、NAND Flash。教材里分过类,在这里全都对得上号。 就连中断,也不过是硬件在某个事件发生时,硬生生把 PC 当前值压栈,然后把 PC 强制指到一个预设的中断入口地址,CPU 就跑去执行中断函数了——计算机组成原理里的“中断响应周期”,在你这片 51 上原原本本地发生。
真棒,大学都学过的。知行合一,知道计算机里面有什么,单片机怎么把这个过程实现,怎么使用这些知识。
4. 知不知道又怎样?抽象叠着抽象
可是,上面这些东西,你不知道,也不妨碍你敲出能跑的 C 代码。
你写 EA = 1;,开启了总中断。
你写 P0 = 0xFF;,把 P0 口的 8 个 IO 全拉高了。
你写 int a = 0; a++;,本质上就是在操作 RAM 里的某个寄存器(存储单元),C 编译器帮你决定用哪个地址,帮你生成操作它的指令。
你全程都在读写寄存器,只不过 C 语言给你披了一层温柔的外衣。
学计算机的,最会玩抽象。 从物理电平 → 链路帧 → IP 包 → TCP 流 → HTTP 报文,一层一层往上叠,底层难用是吧?那我再给你抽象一层。
51 抽象层次还不算高,你 EA = 1 一眼还能看出是去摸一个叫 EA 的开关。
到了 STM32,HAL 库直接挡在你和寄存器之间:
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
这背后是 HAL 库帮你找到 GPIOA 的基地址,算出偏移,定位到输出数据寄存器,然后往那个 32 位寄存器里写了个 (1<<5)。
抽象!你只需告诉管家“把书房的灯打开”,管家自己就去掰开关了。
5. 抽象下面是什么?从 EA = 1 一路拆到晶体管
那如果我们偏偏要撕开这层抽象呢?
EA = 1; 这个 C 语句,在 8051 的语境下,EA 是个特殊功能寄存器中的一位——总中断使能位。
它在字节地址 0xA8(即 IE 寄存器)的第7位。
注意:这里有个大坑。
如果你想用字节操作开总中断,应该写 IE = 0x80;,也就是把 0xA8 这个字节地址里的最高位置 1。
如果你写 EA = 1;,编译器可能会聪明地把它翻译成位操作指令,而 8051 有一个独立的位寻址空间,其中位地址 0xAF 才是 EA。
你绝对不能写成“操作位地址 0xA8”,那个位地址对应的是 IE 寄存器的第 0 位 EX0(外部中断0使能),你这样一搞,总中断没开,外部中断 0 倒给点着了。
所以,严谨地说:
EA=1 → 汇编是 SETB 0xAF,机器码 0xD2 0xAF。
IE=0x80 → 汇编是 MOV 0xA8, #0x80,机器码 0x75 0xA8 0x80。
两者都能开总中断,但走的路不同,寻址方式不同。这就是细节。
好,我们把 SETB 0xAF 这条汇编指令翻成机器码。
查 8051 指令集:SETB bit 的指令编码是 0xD2,后面跟一个直接位地址。
位地址 0xAF,二进制就是 1010 1111。
所以整条指令的机器码是:
1101 0010 1010 1111 (0xD2 0xAF)
这两个字节被烧录进 Flash 的某个地址,比如 0x0000 和 0x0001。
上电,PC=0,从 Flash 地址 0 取出 1101 0010,塞进译码器。
译码器一看:“1101 0010?这是 SETB 指令,后面还得再取一个字节。”
PC 自动加 1,从地址 1 取出 1010 1111,译码器明白:“要去位寻址空间里,把位地址 0xAF 拉高。”
内部硬件选中位地址 0xAF 所对应的那个锁存器,把它电平拉到高。
这个锁存器恰恰好连着中断控制逻辑的总闸门,闸门一开,整个中断系统就激活了。
从你敲下 EA=1 到中断闸门打开,多米诺骨牌一张没塌,全按你设计的顺序倒下。
6. 那到底要操作哪些寄存器?怎么查?
这时候你问:“操作哪些寄存器?” 答:翻数据手册。一本几百页的 PDF,拆穿了就是一本开关面板说明书。
以经典 51 为例,这几个面板你必须摸透:
6.1 IO 口面板
P0, P1, P2, P3。
写 1 就是输出高电平(同时引脚内部弱上拉),写 0 就是输出低电平。想读引脚状态?直接读这个寄存器就行。
6.2 中断调度面板
IE(中断使能寄存器,字节地址 0xA8):
- EA (IE.7):总闸
- ET0 (IE.1):定时器0中断分闸
- EX0 (IE.0):外部中断0分闸
……
IP(中断优先级寄存器,字节地址 0xB8): 哪个中断优先级高,在这里调。TCON(定时器/计数器控制寄存器,字节地址 0x88): 里面有定时器的启停开关(TR0、TR1),还有外部中断的触发方式选择(IT0、IT1)。
6.3 定时器车间
TMOD(定时器模式寄存器,不可位寻址):
决定定时器0/1是当定时器还是计数器,以什么方式工作(13位/16位/8位自动重装)。
TCON:启停控制,溢出标志也在这儿。
TH0/TL0, TH1/TL1:装初值的地方。
想用定时器产生精确的时间间隔?在这几个开关和计数器里填好数就行。
6.4 串口收发室
SCON(串口控制寄存器,字节地址 0x98):
选模式(8位/9位),允许接收(REN),发送/接收中断标志。
PCON(电源控制寄存器,字节地址 0x87):
SMOD 位控制波特率是否加倍,其他位还有掉电模式、空闲模式。
SBUF(串口数据缓冲器,字节地址 0x99):
写 SBUF = 发;读 SBUF = 收。地址虽是一个,物理上是两个独立的寄存器,发收分离,硬件自己判别。
6.5 复用说明(以增强型51或ARM为例)
经典 51 的引脚功能是硬件绑死的:P3.0/P3.1 固定是 RXD/TXD,一打开串口,这两个脚自动切换到第二功能,不用额外设置。
但后来的 STC15/STC8 或者 STM32,有了专门的引脚功能切换寄存器(如 P_SW1、AFIO),你才能“操作 SFR 就能设置引脚复用”。
这个细节知道就好,别在新手期对着 AT89C51 手册找复用寄存器。
查这些开关的方法极简单: 打开手册的 “Special Function Registers” 表格,或看每个外设章节的寄存器描述,一张表告诉你寄存器名、地址、每一位叫什么、写0写1分别是什么意思。 你要做的,就是对着那张表把二进制拼出来。
7. 从51跳到ARM,无非开关面板变大了
有了 51 的底子,去碰 STM32,你会恍然发现,所有套路一模一样,只是:
- 寄存器变成了 32 位,一口气能控制 16 个 IO 口的模式、上下拉、输出速度。
- 每个外设前面多了一道时钟闸门:你得先往
RCC相关寄存器里写个 1,把外设的钟敲响,它才开始工作。51 不用,51 的外设钟大多默认是通的,或者跟主时钟捆在一起。 - 中断控制器变得更复杂,NVIC,还要设优先级分组,但灵魂还是那套:使能位、标志位、优先级位。
STM32HAL 库呢?
它就是在这一大片寄存器面板上,又给你糊了一层墙纸。
你调用 HAL_UART_Transmit(&huart1, pData, Size, Timeout),HAL 在后头帮你查状态寄存器,帮你把一字节塞进数据寄存器,帮你等到发送完成。
“不直接操作寄存器”,不代表寄存器不存在,只是有人替你动了手。
当你要调一个极限性能的中断驱动,或者被一个 bug 卡住时,你还是得掀开墙纸,看背后的寄存器波形。
8. 最后的最后:把整个流程串成一张图
单片机 = 一堆房子(外设、存储器)挂在总线走廊上,每个房子都有门牌号(地址),门牌号后面是一排开关(寄存器)。 CPU 这个快递员,按照 Flash 里的一张张指令单(机器码),到各个房子取货(读寄存器)、送货(写寄存器)。偶尔让 DMA 这小哥代劳搬大件。
你写的每一行 C:
P0_1 = 0; → 编译器把它变成 CLR 0x91(位地址),机器码 0xC2 0x91 → 烧进 Flash → PC 指向它 → 译码器认出“清0位” → 硬件把位地址 0x91(P0.1)锁存器拉低 → P0.1 引脚输出低电平 → LED 灭。
全过程无魔法,全是大学里教过的硬核知识。
什么译码器、ALU、PC、栈、哈佛结构、冯诺依曼结构,全在你手里这块几块钱的芯片里原原本本跑着。 你问我怎么学单片机? 无非就是:翻开手册 → 找到对应的寄存器 → 根据协议要求算出要写的值 → 写进去 → 世界就动起来了。 这份“罢了”的背后,正是你从底层到应用层自由穿行的底气。