0%

点灯大师在线点灯之 STC89C51

闲云潭影日悠悠,物换星移几度秋。阁中帝子今何在?槛外长江空自流。

转眼间又过去了两年有余,时光匆匆,在习惯了被生活拍打向前奔跑的这些日子里,已经很难再挤出感性的文字,似乎从一个诗人变成了一介蛮夫。最近在高温和疫情的双重压迫下,越发觉得需要静下心来,还是要做一个诗人,这样才能写出像诗一样优美的代码呀。不过,在成为真正的诗人之前,请允许我先修炼成 点灯大师。人生路漫漫,难免会走入窄道暗巷,希望在你一筹莫展看不清前方时,我这点亮的每一盏灯,能成为你心中的火、眼中的光,再次、再一次找到方向!

今天对这荒废了很久的博客进行了一些修整,比如:

  1. hexo 升级到了最新版本
  2. 去除了满是广告的畅言评论,换成了 Valine,不太好做数据同步,所以呃..
    • 所有历史评论全都丢失了
    • 所有文章的浏览统计全部丢失了
  3. hexo 的主题 next 升级到了最新版
  4. 将头像从金木研换成了陆光 (好像是白头发的我都喜欢)

还有一些小的细节调整就不一一列出来了,遗憾的是那些五湖四海网友的评论丢失了,如果你们无意间看到了我的更新,如果你们还记得当初是怎么评论的,可以尝试着帮忙再评论回来的吧(🐶。

在这个系列开篇前,废话似乎有点多了,那么就正式进入点灯大师修炼的第一步吧。

点灯的背景

所谓的点灯,其实就是在特定的芯片开发板上,点亮板载的 LED 灯,这是嵌入式开发的范畴。最近在 B 站或一些其它平台上,创客类的视频比较火热 (比如稚晖君),如果能借此机会了解一些基础的嵌入式开发,闲暇时间搞一些小发明,说不定就打开了另外一扇窗呢?并且,我相信这是理工男都喜欢的东西。

在嵌入式开发里,点灯的地位相当于传统应用开发里的 Hello World,如果你是做应用开发的,那么你应该清楚这是怎样重要的一个存在。

以下便是 C 语言的 Hello World
1
2
3
4
5
6
#include <stdio.h>

int main(int argc, char *argv[]) {
printf("hello world\n");
return 0;
}

如果只是单纯的将一个小 LED 点亮,那自然是成为不了 点灯大师 的,所以,我会在点灯的过程里,再额外阐述些更值得关注的点,让你能点亮灯,又不止是点亮了灯。

STC89C51 简介

和 51 单片机的关系

STC89C51 是 STC80C51 单片机的升级版本,而 STC80C51 就是传统意义上的 51 单片机,51 单片机是大多数嵌入式开发的启蒙单片机。古老但简单,可以快速的上手并了解基础的单片机开发套路,所以,从这块板子开始点灯,我觉得是个不错的选择。

STC89 系列参数

STC 是由宏晶科技推出的一系列芯片,官网地址是:http://www.stcmcudata.com,当然这官网的布局花哨且杂乱,就不在本篇的讨论范畴了。以下是官方对于 STC89 系列单片机的介绍:

  1. 增强型 8051 单片机,6 时钟/机器周期和 12 时钟/机器周期可任意选择,指令代码完全兼容传统 8051
  2. 工作电压: 5.5V-3.3V(5V单片机) /3.8V-2.0V (3V单片机)
  3. 工作频率范围: 0~40MHZ,相当于普通 8051 的 0~80MHz,实际工作频率可达 48MHz
  4. 用户应用程序空间: 4K/8K/13K/16K/32K/64K 字节
  5. 片上集成 1280 字节或 512 字节 RAM
  6. 通用 I/O 口 (35/39个)
    • 复位后为: P1/P2/P3/P4 是准双向口/弱上拉 (普通 8051 传统 I/O 口)
    • P0 口是开漏输出,作为总线扩展用时,不用加上拉电阻,作为 I/O 口用时,需加上拉电阻
  7. ISP(在系统可编程)/IAP(在应用可编程),无需专用编程器,无需专用仿真器可通过串口 (RxD/P3.0,TxD/P3.1) 直接下载用户程序,数秒即可完成一片
  8. 有 EEPROM 功能
  9. 看门狗
  10. 内部集成 MAX810 专用复位电路 (HD 版本和 90C 版本才有),外部晶体 20M 以下时,可省外部复位电路
  11. 共 3 个 16 位定时器/计数器,其中定时器 0 还可以当成 2 个 8 位定时器使用
  12. 外部中断 4 路,下降沿中断或低电平触发中断,Power Down 模式可由外部中断低电平触发中断方式唤醒
  13. 通用异步串行口 (UART),还可用定时器软件实现多个 UART

值得关注的是 3、4、5 这三点描述的参数,我这里特意给它加粗了。可以看到相比于应用开发而言,这种单片机的资源要少得可怜,无论是处理器的计算能力、用户程序的大小、程序运行时的内存空间都很的短缺。所以,无论是在编程实现还是产品选型上,这些参数都是需要考虑的点。

开发板简介

上一小节,我们对 STC89C51 做了一个简单的了解,但光有这块芯片我们是无法点灯的,毕竟只是一块芯片,连灯都没有呀!在嵌入式开发里,我们一般将这种芯片称之为 MCU,当然还有一种可以称之为 MPU 的芯片。

基础概念

在进入开发板介绍之前,我觉得有必要先了解一些基础概念,这也是成为点灯大师的必经之路。

MPU 和 MCU 的差别

首先是 MPU、MCU,虽然这两者很多人都混着用,但还是存在一些细微的差别:

  1. MPU: 全称是微处理器单元 (Micro Processor Unit),相比于 MCU 而言,它有更强的计算能力,一般可以运行实时操作系统 (RTOS),并且可以有内存管理单元 (MMU)
  2. MCU: 全称是微控制器单元 (Mirco Controller Unit),相比于 MPU 而言,它不能胜任较复杂的计算任务,更多的只是应用于外围设备控制,一般不能运行操作系统,且不会有内存管理单元

简而言之,MCU 就是比 MPU 计算及处理能力要弱很多的芯片,但价格便宜呀,所以也会有适用的场景。而,我们的 STC89C51,就是一块 MCU。至于什么是 MMU、什么是 RTOS,以及更多的差异点,后续如果涉及到的话,我们再展开讨论。

SoC 又是什么

前面讲了 MPU、MCU 这种微芯片,这里又要引出来另外一个概念叫 SoC,SoC 是什么呢?SoC 的全称是片上系统 (System on Chip)。这里我以大家比较熟知的 ARM 处理器来举例,大家有没有想过,很多手机或平板都在号称自己使用的是 ARM 处理器,但同样是 ARM 处理器,为什么会有高通、三星、苹果等等品牌的区别呢?比如高通的骁龙、三星的 Exynos。 这就要将处理器再细分一下了,ARM 公司其实并不实际生产处理器,而是提供处理器的架构设计方案,各个厂商 (比如高通或者三星) 拿到 ARM 公司的设计以及版权后,就可以开始自己的处理器设计,这里的设计并不是对 ARM 的设计进行修改 (应该也是版权不允许的),而是使用 ARM 架构所预留的接口(或者称之为地址空间)来扩展芯片上的外围设备 (简称芯片外设),比如加一些控制单元、处理单元等等。这就是片上系统,片上系统所能拓展的外设也很多,比如:GPIO、UART(串口)、I2C、I2S、SPI 等等,这些都称之为芯片的外设(Peripheral),下图是恩智浦的一款处理器片上系统框图:

红色框里的,可以说是实际意义上的 CPU,是由 ARM 公司提供,其它所有的框块都是由恩智浦设计实现,最终一起打包生产 (交给台积电这种),我们最终能看到的就是一个非常小的小黑块。所以像三星、高通、恩智浦这种,一般也称他们为 SoC 厂商,由他们给 CPU 内核增加外设,这样才能产生真正的实用价值。而我们今天使用的 STC89C51 也是类似,只不过它的核心不是使用的 ARM,而是 Intel。

什么是开发板

讲完了 SoC,再来讲开发板就更容易让人理解了。就像上面所说的,如果 ARM 直接给你一块 ARM 的处理器核心,或者 Intel 给了你一块它的核心,以你一个人的能力,大概率是没法去直接使用的。那如果是 SoC 厂商给了你一块 CPU、MPU、MCU 呢?这个要使用起来就简单了很多,但也不是那么容易,一般情况下你需要先通过 SoC 厂商给的芯片手册了解清楚处理器里的外设,以及电器属性;然后构建最小系统电路,最小电路一般包括下面几个部分(简单看看,现在不了解也无所谓):

  • 电源电路: 这个很好理解,这个电路就是供电的,没电你的处理器肯定是没法工作的
  • 复位电路: 可以理解为一个重启按钮,让你的芯片能够正常的重启工作
  • 晶振电路: 这个并不一定是必须的,有些芯片需要外接晶振提供时钟源才需要 (比如我们今天的这个 STC89C51)
  • 下载电路: 就是将编写的程序能够下载到指定的地方 (要看芯片启动逻辑而定),让处理器能够读取运行的

有了最小系统电路后,理论上你的芯片就可以工作起来了,但没有灯呀!所以除了最小系统之外,就是看你的需求而定了,要接入哪些板级设备,板级设备就是跟你处理芯片连接在一起的设备,比如我们要接一个 LED 灯,或者接入一个蜂鸣器,又或者接入一块 LCD 显示屏,等等。

等板级设备考虑完成之后,你就可以进行电路设计了,先画原理图,再进行 PCB 设计,然后找厂商进行 PCB 打样,打样的同时你可以并行的购买需要的元器件 (比如我们的 LED 灯)。在收到厂商打样回来的样板之后,你就可以进行元器件焊接工作了,不过焊接一个处理器还是挺有成就感的,是不是?当然不可能全是靠电烙铁进行一个针脚、一个针脚的焊接,焊接这个就不展开了。一切准备就绪后,你就可以进行代码编写了,然后下载到板子上进行测试。都没有问题后,就可以考虑设计个外壳,然后组装起来,这就是一整个使用 SoC 厂商给到的芯片流程了。

是不是觉得流程还是太长了,并且对于初学者而言并不友好,所以这里就引出了另外一个东西,叫 开发板。随着 SoC 厂商的芯片复杂度越来越高,想要能够靠一己之力将一块芯片驱动起来已经越来越难,一般情况下 ARM 架构的处理器芯片手册基本都在 4000 页以上,并且有些内容 SoC 厂商也是不会开源的。于是在 SoC 厂商的下游又诞生了另外一个层次:定制方案商或开发板商。SoC 厂商在推出一款新的处理器后,基本也会推出一款对应的示例开发板,所谓开发板就是包含了上述最小系统电路以及部分的板级外设,拿到后插电就可以开发使用的那种。如果是能运行 Linux、Android 这种比较庞大的系统,SoC 厂商还会提供他们移植好的 BootLoader 以及 Android/Linux 系统,当然这些 SoC 厂商基本不会直接公开给个人 (部分会在官网提供下载),但开发板或方案商能拿到这些源码和资料。开发板厂商会参考 SoC 厂商提供的开发板进行定制,要么直接做出各种产品进行售卖 (比如手机、平板、智能音箱等),要么直接卖大而全的开发板给嵌入式开发者进行学习使用(就是我们这种人了)。SoC 厂商有时候又被称之为原厂,比如在学习 Linux 驱动时,会经常在 原厂 提供的设备树基础上进行修改。

既然励志成为点灯大师,那我们先将关注点转移到怎么点灯,而不是怎么造灯。所以,先买一块开发板,下图是我手头上的一块普中开发板:

这里包括后续使用到的所有开发板,为了避免推销嫌疑,都不会给出购买链接的,我们把注意力集中在点灯上。上图中间那黑色的一条就是 STC89C51 的本尊了,其它都是一些板级外设。这块开发板有一个特点,所有外设都不是直接与 MCU 连接的,而是 MCU 引脚连接到了排针上 (或者叫端子),外设的引脚也是连接到排针上,所以我们需要使用额外的导线将需要的外设与 MCU 对应的引脚通过排针进行连接,这种设计就比较灵活,我们甚至可以通过导线连接到本身不在开发板上的设备。下图是今天我们需要点亮的 LED 灯所在区域:

可以看到,这里有好多的灯,并且摆放的很像人行道旁边的红绿灯,我们可以用程序来模拟红绿灯变化情况,不过,这不在本篇的范畴,本篇我们只要点亮一盏灯即可!

原理图

在点灯之前,我们需要看下开发板厂商给我们提供的原理图,确定下要怎样才能将灯点亮,以下就是这块 LED 区域的原理图:

可以看到这批 LED 灯的一端连接的是 VCC,也就是高电平,另外一端连接到了 J18、J19 两个端子上了,我们要点亮它,只要将 J18、J19 其中任意一个排针给上低电平,就可以亮起来了 (电流从高往低流动起来)。那么怎么给它低电平呢?一般 MCU 的引脚中都包含了一批称之为 GPIO 的引脚,全称就是通用输入输出,这批引脚可以输出高低电平,也可以检测输入的电平高低,当然都是靠编写程序来驱动的。在介绍 STC89C51 时,官方已经给出了这样的说明:

  1. 通用 I/O 口 (35/39个)
    • 复位后为: P1/P2/P3/P4 是准双向口/弱上拉 (普通 8051 传统 I/O 口)
    • P0 口是开漏输出,作为总线扩展用时,不用加上拉电阻,作为 I/O 口用时,需加上拉电阻

关于上拉、下拉、推挽、开漏这些电器属性,本篇不作展开,可以自行搜索学习。我们这里知道了,P0/P1/P2/P3/P4 这些都是 GPIO 接口 (P4是后续的某些升级板子上才有的,我手头上的这块并没有这个接口),这样就好办了,还需要再看看原理图,这些 GPIO 口连接到了哪里:

就如前面所说,这些引脚都接到了端子上,值得一提的是上图中还包含了一个复位电路,并且将复位信号也接到了端子上,可以用来扩展其它芯片的复位。那么我们所关注的 GPIO 连接分别是:

  • P0 - J22
  • P2 - J25
  • P3 - J29
  • P1 - J20

需要注意的是,这些每一个都是一组 GPIO,每一组包含了 8 个 GPIO,比如 P0 这组就是 P00 - P07,其它类似。所以我们只需要挑选一个引脚即可,这里我们选择 P00,最终是接入到 J22 的 1 号排针上,然后 LED 选择 D1 这个绿色的灯 (不要问为啥选择绿色),D1 对应的是 J19 的 1 号排针,拿出杜邦线连接后如下图:

硬件准备就绪后,我们下一步就开始准备点灯了!

开始点灯

在开始编码点灯之前,我们还要准备下开发环境,已经很久不用 Windows 了,那就看看如何基于 MacOS 来搭建点灯环境吧!

开发环境准备

首先我们要安装的肯定是交叉编译器了,要在 MacOS 编译 51 单片机,我们需要安装 SDCC 工具链,可以在以下地址下载安装:

安装方式比较简单,这里就不赘述了。安装完工具链,我们还需要能将编译出来的产物下载到开发板中,在 MacOS 上我们可以使用 stcgal 这个工具,相关地址如下:

stcgal 是通过 python 来编写的,所以系统中的 python 环境要搭建好。既然要成为点灯大师,我索性重新用 C 写了个原生的下载工具,我将其称之为 burnit,当前阶段只提供了可执行文件,后续等时机成熟了,再一次性开源出来,可以通过以下链接下载:

命令行的使用方式遵循了大多数 GNU 工具规范, 命令格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
❯ ./burnit --help
Usage: burnit [OPTION...] [BINARY FILE]
A tool that can burn MCU on MacOS

Serial Port Options:
-b, --baudrate=<BAUDRATE> Config serial port baudrate
--bits=[BITS] Config serial port bits, default is 8
valid option is 5|6|7|8
--parity=[PARITY] Config serial port parity, default is 'none'
valid option is 'none'|'even'|'odd'
-p, --port=<PORT> Config serial port address
--stopbits=[STOPBITS] Config serial port stopbits, default is 1
valid option is 1|1.5|2

Burning Options:
-a, --address=<ADDR> Config burn address
-B, --board=<BOARD> Config which MCU board to use

Misc Options:
-?, --help Give this help list
--board-supports Show all supported MCU boards
--usage Give a short usage message
-V, --version Print program version

该工具只支持串口的方式来下载程序,所以可以看到,大部分都在配置串口相关参数,本次使用的开发板中已经集成了一块 USB 转串口的芯片: CH340G,值得一提的是较新的 MacOS 系统中已经内置了 CH340 驱动,所以无需做额外的驱动安装。原理图如下:

普中在这块的下载电路里还做了一个额外的小设计,可以看到 CH340 的 RTS、DTR 引脚通过限流电阻以及三极管最终连接到了一块场效应管上 (Q3),并且场效应管和前面的开关电路连接,从而可以通过用程序控制 RTS、DTR 来进行电源开关。STC89 需要开关电源后才能进入下载模式,有了这样的设计,就不需要每次烧录程序的时候手动拨动开关了,当然 stcgal 这个工具里没有适配这样的设计,而我们自己写的这个 burnit 就可以更加轻松的下载程序了。

开始编写程序

终于到了最关键的一步,我们要写代码来点亮这盏期待已久的 LED 灯了,直接上代码吧:

led.c
1
2
3
4
5
void main() {
__sbit __at (0x80) P0_0;
P0_0 = 0;
while(1);
}

使用 sdcc 编译该代码:

sdcc led.c -o led.hex

将编译产生的 led.hex 文件烧录到开发板中,使用我们的 burnit 命令:

./burnit --baudrate 9600 --port /dev/cu.wchusbserial14140 --board stc89 led.hex

注意:上述命令中 --port 之后的串口地址,需要根据实际情况来定,可以通过 ls /dev/cu.* 来查找正确的开发板串口对应地址。当终端出现以下输出时:

1
2
3
4
5
6
7
8
9
...
>> writing binary file (106 bytes)...
100 % [##################################################]
>> writing binary file done !
>> setting option...
>> setting option done !
>> reset MCU...
>> reset MCU done !
>> disconnected !

标示着整个烧录过程结束,burnit 工具会自动重置开发板,所以这时候这颗 LED 灯应该就亮了起来:

这是我为你们点亮的第一盏灯,绿色代表着能够一路畅通无阻,期望迷茫的你能找到绿色指明方向。不过为什么短短 5 行代码,它就亮了起来呢?这是接下来我们要讨论的,也是成为点灯大师必修的基本功!

什么是寄存器

首先我们要了解清楚的是,什么是寄存器?如果你只做过传统的应用开发,那你了解到的寄存器概念可能是狭义的,传统意义里的寄存器大多是指处理器核心里的寄存器,可以称之为 CPU 寄存器,比如 ARM 的通用寄存器 r0、r1、r2…,Intel 的 eax、ebx、ecx…。前面说过了,SoC 厂商在拿到这些处理器核心后,会开始构建片上系统,也就是基于处理器核心来增加一些额外的处理或控制单元 (比如 GPIO),那 SoC 的用户怎么使用这些单元呢?也是通过寄存器,SoC 厂商会将这些单元的配置和使用都定义成一系列的寄存器,这就是广义上的寄存器,可以称之为片外寄存器 (片外是相对于处理器核心而言)。因为片外系统需要适应广泛的使用场景,所以一般模块种类和配置方式都很多,因此,SoC 厂商在提供芯片的同时,也会提供对于片外寄存器进行配置使用的描述手册,大多数情况下都会放在数据手册里 (datasheet),也有直接提供寄存器手册的。这些描述会占据手册的大部分类型,以下是瑞芯微的一款芯片数据手册对于 GPIO 相关寄存器的描述:


可以看到,只是 GPIO 的寄存器就占据了 200 多页的篇幅,所以数据手册只能作为类似于字典来查阅,死记硬背几乎是不可能的事情,也没有必要。那么这些片外寄存器和 CPU 核心是怎么交互的呢?我们虽然知道了这些概念,但到底怎么通过程序来使用它呢?这就要了解下什么是地址了!

什么是地址

说到地址,应用开发者的第一反应应该就是指针了,指针是什么?指针就是指向某块内存地址的变量,我们可以通过指针直接操作某块内存。这里也引入了一个地址的概念,也就是访问指定内存区域的唯一编号,如果再细究一下,这里的地址又可以分为:虚拟地址、物理地址。一般情况下,站在一个高层应用的视角,它所能看到的地址都是虚拟地址,比如 0x02020303,可以简单认为这就是一个哈希表的 key,系统内核的内存管理模块 (一般使用软件或硬件 MMU 实现),会通过这个 key 查询到一个 value,而这个 value 就是真实要去访问的地址,也就是物理地址。要深入了解虚拟地址的话,就要牵扯到操作系统的相关设计,这个话题也比较庞大,为了和后续我们所讨论的地址区别开,这里只是简单提一下。作为点灯大师,我们关注的,当然是最接近灯的物理地址

要想比较透彻的了解物理地址,我们还是要回过头来看看处理器核心的设计,SoC 厂商在设计处理器核心外设时,最主要关注的就是以下三点:

  • 时钟源: 可以简单理解为 CPU 的心脏,时钟的振动驱动着 CPU 取指、解析、执行
    • 这里涉及到几个周期的概念:时钟周期、指令周期、机器周期、总线周期,这里就不展开讨论了
  • 地址总线: 寻址信号线,类似于以往的电话线,寻址就好比喻拨号,拨出一个特定号码后,连接在另一头的特定电话就会被唤起
  • 数据总线: 用于数据读写的信号线,站在 CPU 的角度而言,就是用于操作对应地址的“内存”

那么这里的地址总线上,连接的就是 SoC 厂商所设计的片外模块,通过对模块进行地址分配,地址总线上发出特定的寻址信号时,相应的模块就会感应到自身被呼叫,后续通过数据总线进行数据交换。而模块的地址分配,一般是一个范围的,比如 0x02000000 - 0x0200F00 这个区域是给 I2C 外设、0x03000000 - 0x0300F00 这个区域是给 GPIO 外设等等(这里不是实际数据,地址也不是固定的,要看 SoC 厂商的定义了,只是举个例子)。而每个区域里,又会细分地址,这就是关联到片外寄存器了,每个寄存器都对应了一个实际的物理地址,通过对这个物理地址的操作,就可以直接操作寄存器!以下是某一款 MCU 的地址框图:

从图中可以看到,这是一个 32 位的地址空间,外设的地址空间是: 0x4000 0000 - 0x4002 4000;一共有 4 组 GPIO 接口:PortA - PortD,地址空间是: 0x4001 0800 - 0x4001 1800。另外可以看出,作为一个 MCU,很多地址空间都是没有被使用,因为本身的能力决定了它使用不完。这里又无意间引出了另外一个概念: 32 位地址空间 (坑越挖越多呀,感觉今天的灯还要再点一会儿)。

我们常说一个处理是 32 位、64 位,甚至我们今天还说了,STC89 是一个 8 位的处理器,那这个 8 位是指啥呢?是前面我们所说的 32 位的地址总线么?并不是!这里的位数是指数据总线的位数,一般情况下地址总线的位数都是和数据总线相匹配的,但也有例外,比如我们今天所使用的 STC89,虽然数据总线只有 8 位,但地址总线却是 16 位的带宽。我们可以回过头来,看看宏晶所给的 RAM 参数:

  1. 片上集成 1280 字节或 512 字节 RAM

大家之前有想过 8 位的处理器为啥 RAM 可以超过 256 字节么?这就是因为它的地址总线是 16 位的,寻址范围是在 64K 以内。通常而言,地址总线的宽度决定的是扩展性,这显而易见,地址越多,所能编码的外设空间就越大;而数据总线的宽度才是真正决定处理器的处理性能,比如 8 位的数据总线,需要分多次才能完成一个 32 位数据的传输,而 32 位宽度的一次性就可以传输完毕。因此,数据总线的宽度,通常就是我们所说的处理器位数。这里还有一个问题是需要解答的,地址总线的寻址范围是有限的,比如 16 位是 0 - 64K,32 位是 0 - 4GB,那么我们这些外设占用了部分的地址编码,那传统意义里的内存怎么办呢?32 位处理器是不是就达不到 4GB 内存了呀?这里就又引出了两种编址方式:

  1. 独立编址
  2. 统一编址

相关的,CPU 结构也有两种类型:

  1. 冯诺依曼结构
  2. 哈佛结构

关于这两种结构就不展开讨论了,我们主要看看这两种编址方式的差异。从处理器内核的角度而言,所有与外设的交互通道都可以看成 IO 口,IO 口可以连接到 SoC 厂商所设计的各个模块 (比如串口、通用输入输出接口等),与 IO 所对应的就是主存单元,可以理解为就是传统意义里的内存。那么地址总线需要关注的就是这两块: 要寻找的是哪个 IO 口,要操作的是哪块内存,既然是地址总线,当然都是通过地址来定位。

统一编址就是让 IO 口和主存单元使用同一套地址编码,这样 IO 口就会占用掉部分主存单元的地址空间,也就会导致 32 位的处理器实际主存空间不会达到 4GB。优点是操作 IO 口和操作主存使用同一套内存指令即可,处理器设计上会简单很多。

相对的,独立编址就是让 IO 口和主存单元使用独立的地址范围,双方地址可以重合。这种编址方式不会占用主存单元的空间,为了区分是操作 IO 还是主存单元,就需要引入额外的操作指令,处理器的设计上会复杂很多。

另外一点,独立编址和统一编址并不是互斥的,也就是说在一片处理器上,可能存在混用的情况。比如我们所使用的 STC89,像 GPIO、串口这种,称之为特殊功能寄存器 (SFR),采用的就是统一编址;而片外和片内的 RAM 所采用的就是独立编址。前面也说过,大多数 MCU 的地址空间都是使用不完的,所以大多数的 MCU 都是采用的统一编址,毕竟一块小小的 MCU,你也不可能指望它有多大的主存单元。随着处理器的位数越来越大,统一编址也必然成为首选,毕竟地址空间太多了,比如 64 位的处理器,一般采用的都是 48 位的地址总线,理论上寻址能到 256TB !

点灯的原理

最终再回到我们今天所讲的 STC89C51,它的 P0 就是一个特殊功能寄存器,这个寄存器地址是 0x80,STC89C51 的一个寄存器占用的是 8 位,每一位对应一个 GPIO,所以可以推导出 P0 这一组 GPIO 实际的地址:

  • P0.0 - 0x80
  • P0.1 - 0x81
  • P0.2 - 0x82
  • P0.3 - 0x83
  • P0.4 - 0x84
  • P0.5 - 0x85
  • P0.6 - 0x86
  • P0.7 - 0x87

再回到我们这一段点灯代码:

1
2
3
4
5
void main() {
__sbit __at (0x80) P0_0;
P0_0 = 0;
while(1);
}

这其中的 __sbit __at (0x80) P0_0 是 SDCC 特有的语法,表示将 P0_0 映射到 0x80 作为一个 位寄存器(Bit Register),所谓的位寄存器估计也只有 51 单片机里有这样的概念了,其实就是代表 P0_0 只能操作一位,也就是只能输入 0 或者 1,0x80 地址对应的就是 P0.0 这个 GPIO,我们将其设置为 0 就代表输出了一个低电平,从而让 LED 灯点亮。相对于位寄存器而言,就有一个叫做字节寄存器(Byte Register)的概念,可以使用 __sfr __at (0x80) P0 来定义,如果我们将这个 P0 = 0; 的话,那从 P0.0 - P0.7 全部都会输出低电平,相当于对所有位寄存器进行了一个操作。

除了使用 C 语言来点灯,我们还可以直接使用汇编,这样生成出来的 ihex 文件更小:

led.asm
1
2
3
4
5
6
7
8
9
10
11
12
.module led
.optsdcc -mmcs51 --model-small

.area RSEG (ABS,DATA)
.org 0x0000
P0_0 = 0x0080

.area CSEG (CODE)
clr P0_0

_loop:
sjmp _loop

关于汇编这里就不展开讨论了,有兴趣的可以自行研究,这里说一下编译方法:

sdas8051 -og led.asm
sdld -i led.hex led.rel

最终将生成的 led.hex 通过我们 burnit 烧入到开发板,依然能看到那颗闪亮的绿灯!

总结

作为点灯大师修炼的第一步,虽然已经尽量去普及基础概念了,但难免会有遗漏,有必要补充的内容,会在后续点灯的路途里再娓娓道来。那么这第一盏灯带来了哪些收获呢?

  • MCU、MPU
  • SoC
  • 寄存器
  • 地址
  • GPIO

修行之路是苦涩的,但点灯是快乐的,愿大家能早日点亮心中的那一盏灯。