框架与要点
编写单片机类的程序,最核心、最重要的是上面的框架。
硬件支持:单片机
软件框架
初始化代码
时钟配置
定时器配置
中断配置
功能代码
通过此框架我们会明白,初始化代码是重中之重。实际初始化代码是初始化寄存器配置。
时钟配置,是初始化时钟相关的寄存器配置
定时器配置,是初始化定时器相关的寄存器配置
中断配置,是初始化中断相关的寄存器配置
这里引申出一个重要概念,寄存器。
展示的时间到了,实际上寄存器就是一排开关,一排管道,一排过滤器。
如果寄存器的一位(一个开关)是红色的,代表这个这个位管理这个通道(每个色块)关闭,如果是绿色的代表这个通道打开,当寄存器1~5的一位都变成绿色,蓝色的电子才能通过,进入后续的电路通道。
编写单片机程序,实际大部分工作就是在配置这些通道。
什么情况下打开通道,什么情况下关闭通道
什么时间打开通道,什么时间关闭通道
同时打开几条通道,关闭几条通道
检查这个通道的状态,是关闭了还是开着
对以上措辞转换个说法:
什么情况下打开寄存器的某位(置位),什么情况下关闭寄存器的某位(清零)
什么时间打开寄存器某位(置位),什么时间关闭寄存器某位(清零)
同时打开寄存器的多个位(置位),关闭寄存器多个位(清零)
检查寄存器某位的状态(读取)
寄存器操作是优先于其他功能代码实现的,因为这些管道不打开,其他功能实现不了,例如我要控制某个单片机引脚输出电流/电压(变成高电平)首先要配置这个引脚的控制寄存器能够输出(实际也可以控制器关闭或者输入),部分单片机还可以控制其输出电流的大小,然后才能写代码控制引脚变为高电平。(部分51单片机内部做了这个动作,不用控制这个寄存器,可以直接控制读取引脚输出、输入)
//注意以下代码不是stc系列51的控制,是另一款51的配置方式,仅做参考 //P4OE就是P4的控制寄存器 //P4的第一个引脚设为输出,其他全部为输入引脚 P4OE |= 0B11111101; P4_1=1; //P4_1 引脚输出高电平
单片机编程大约70~80%的工作是在控制寄存器操作(配置、读取)理解了寄存器的配置原理,懂点基础C语言,剩下的编程是水到渠成的工作。
寄存器和数字电路知识再参考下面的第一个链接:
你管这破玩意叫 CPU ? - 码农的荒岛求生的文章 - 知乎 码农的荒岛求生:你管这破玩意叫 CPU ?
你管这破玩意叫编程语言? - 码农的荒岛求生的文章 - 知乎 码农的荒岛求生:你管这破玩意叫编程语言?
因为开设单片机训练营用的单片机是STC89C52RC(硬件支持),所以用它为例将寄存器初始化配置。这样保持和课程一致,实际其他51类的单片机是类似的。选择此款单片机另一个初衷是它的程序完全可以无须改动或者需要极少的改动就可以在proteus运行,这样不用开发板我们也可以先开始学习单片机编程。
中文版规格书下载链接:https://www.stcmcudata.com/datasheet/STC89C52.pdf
一、时钟配置(晶振选择)
目标是让外部晶振或者内部晶振正常工作。
个人喜欢用人来类比单片机。
第一个解决的问题就是时钟配置。
时钟就是脉搏、心跳,晶振就是心脏,心脏每秒跳动的次数就是频率。
单片机分为内部晶振和外部晶振,也就是说它可以有两个心脏,你可以选择只用一个,并且是心脏跳动的频率你可以选择,这个工作就是时钟配置。STC89C52 只支持外部晶振。
先看单片机实物:
再看它的外部晶振(外部心脏)
然后将他们组合成电路:
这个时候我们再来看电路图
这款产品没什么时钟寄存器配置,直接连上晶振就能用。另外注意,部分单片机时钟配置不是在程序中完成的,而是在编写程序前,在编辑器、下载软件中配置的。
二、定时器配置
目标正确配置定时器,能够让定时器间隔一定时间工作,实现1ms定时。
STC89C52 三个定时器,定时器0、定时器1、定时器2。
学会一个定时器,其他定时器的配置类似,触类旁通,使用定时器0为例学习。
定时器位于单片机内部
定时器需要配置定时寄存器后才能使用
初始化定时寄存器
定时器中断程序处理
定时器功能程序代码编写
初始化定时器的工作先后顺序?
选择那个定时器启用(0、1、2)
设定定时器的计数模式
配置定时器计数值
启动定时器
启用定时器中断
总中断开启
中文版规格书下载链接:https://www.stcmcudata.com/datasheet/STC89C52.pdf (以下图片见189页)
总共TCON、TMOD、TL0、TL1、TH0、TH1, 6个寄存器参与了配置定时器0。
中文版规格书下载链接:https://www.stcmcudata.com/datasheet/STC89C52.pdf (以下图片见190页)
很多时候,我们写规格书的工程师,都是理科人才,理科人才俗成“一根筋”(非贬义,不是这种特质,太灵活而不严谨也做不好技术),他是以自己的智力来衡量大部分人的智力,也就是跳跃式思维写东西,我们看上面简单的寄存器TCON表格,实际上就是不友好的表示方式,SFR name TCON 实际上上面已经说明了这是寄存器TCON 表格里又出现它,增加脑力损耗,应删掉,Address 88H 地址是寄存器集中在一块,每个寄存器都会分配个地址,但是对我们编程来说,基本上不用管它,所以这个也可以不列在这里。然后bit name 又写的太省了,bit应该是位名称(或者称位序号), name应该是功能名称,所以我给他改改。
所以我们看规格书的过程,是从庞杂的信息中提取有效的信息,辨别能力不是一开始就有的,而是我们不断在实践中建立的,所以初学者看规格书会看到头大,就是规格书不是入门书,而是工具书,需要你具备很多专业和边缘知识才能更好的理解,更容易获取到自己有用的信息。
向导推荐逆向学习法,先了解框架,然后先实现功能,倒查为什么这样能够实现。
最上面是写了单片机编程的核心框架,然后我们找到一个完整的程序,先看这个例子,它的实现,然后倒推。
下载STC-ISP stc系列单片机的烧写程序 http://www.stcmcudata.com/STCISP/stc-isp-15xx-v6.88F.zip
它内部有示例代码,我们直接看
分析其示例代码,倒推寄存器设置的要求
//为了利于说明,我把大部分注释代码删除 #include "reg51.h" typedef unsigned char BYTE; typedef unsigned int WORD; #define FOSC 11059200L #define T1MS (65536-FOSC/12/1000) //1ms timer calculation method in 12T mode sbit TEST_LED = P1^0; //work LED, flash once per second WORD count; //1000 times counter void tm0_isr() interrupt 1 { TL0 = T1MS; //reload timer0 low byte TH0 = T1MS >> 8; //reload timer0 high byte if (count-- == 0) //1ms * 1000 -> 1s { count = 1000; //reset counter TEST_LED = ! TEST_LED; //work LED flash } } void main() { TMOD = 0x01; //set timer0 as mode1 (16-bit) TL0 = T1MS; //initial timer0 low byte TH0 = T1MS >> 8; //initial timer0 high byte TR0 = 1; //timer0 start running(定时器0开始运行) ET0 = 1; //enable timer0 interrupt EA = 1; //open global interrupt switch count = 0; //initial counter while (1); //loop }
看上面代码整个TCON 寄存器中,8个位,只用到了一个,TR0=1(启动定时器0,开始运行),其他未用,也就是说我们实现目标1ms定时,很多其他通道(寄存器位)不用去管它,它默认是开是关大部分时候不影响我们的程序运行。
下面逐一反推,定时器寄存器的配置方式
void main() { TMOD = 0x01; //set timer0 as mode1 (16-bit) TL0 = T1MS; //initial timer0 low byte TH0 = T1MS >> 8; //initial timer0 high byte TR0 = 1; //timer0 start running(定时器0开始运行) ET0 = 1; //enable timer0 interrupt EA = 1; //open global interrupt switch count = 0; //initial counter while (1); //loop }
以上是程序的主体(主程序),是单片机的灵魂,也是一个程序从出生活动到死亡的过程。
TMOD = 0x01; //set timer0 as mode1 (16-bit) TL0 = T1MS; //initial timer0 low byte TH0 = T1MS >> 8; //initial timer0 high byte TR0 = 1; //timer0 start running(定时器0开始运行) ET0 = 1; //enable timer0 interrupt EA = 1; //open global interrupt switch
以上6句代码就是做了我们开始说的定时器初始化工作
选择那个定时器启用(0、1、2)
设定定时器的计数模式
配置定时器计数值
启动定时器
启用定时器中断
总中断开启
这一部分全部是寄存器的配置,有些单片机寄存器只能整个的控制,也就是处理一个8位的寄存器,要同时处理8个位,这款51单片机可以处理单个位,上面说的TR0我们看看在哪里。
TR0 在TCON寄存器的第四位,8位寄存器,实际就是8个通道,8个存储区域。每个存储区域只能存放0和1,0代表通道打开,1代表通道关闭。
看这里,寄存器1是怎么配置的,0000 0010 也就是关关关关 关关开关。
TR0=1 代表只是TCON第四位开,其他开关不管它。
规格书:https://www.stcmcudata.com/datasheet/STC89C52.pdf (以下图片见190页)
类似的,现在我们把TMOD、TL0、TH0、ET0、EA都从规格书中找出来,看看它位于那个寄存器或者它是哪个寄存器。
规格书:https://www.stcmcudata.com/datasheet/STC89C52.pdf (以下图片见191页)
注意这里设置TMOD和设置TR0的区别,TMOD是整个TMOD寄存器,TMOD=0x01(十六进制表示)=0b00000001 (二进制表示)这个8位寄存器所有的位通道都配置了,只是填充0还是填充1。TR0=1 只是TCON寄存器第四位填充1,打开一个通道,其他7个通道不管它。
TMOD = 0x01; //设置定时器0 作为16位模式 //set timer0 as mode1 (16-bit)
能设置16位模式,意味着也能设置8位模式、13位模式,这个位数越大,可以计数定时的范围越宽,意味着定时时间可以更长。
TL0 = T1MS; //initial timer0 low byte TH0 = T1MS >> 8; //initial timer0 high byte
上面两句作为一组来看,主要原因是TL0 TH0两个寄存器一般是同时使用的。
TH0 TL0 是定时器的两个计数容器,也就是计数寄存器,计算机、单片机一般以一个字节为单位,一个定时器计数寄存器实际上就是一个字节(8位)的容器,TH0 TL0 两个合起来就是两个字节(16位)的计数容器。
TH0 TL0 都是粮仓,粮食就是填充值,到了一定的数量,触发开关,证明计数完成。计数实际上和定时不分家的,假设我们的输送带匀速往里面送粮食,1秒送一颗粮食,计数到10颗就是10秒,所以定时器又叫做定时计数器。这样定时器可以同时完成两个工作。
实际上简单点我们从1数到10,大家尝试下就明白了,是需要时间的。
假设我们不是从空粮仓开始计数,而是里面已经灌满了半仓,那么我们填满粮仓的时间就减少一半。所以设定TL0 TH0的数值,就可以控制定时的时间。
这里有同学就奇怪了,TL0 =10,TH0=100;我理解,TL0=TIMS,TH0=TIMS>>8什么鬼?
#define FOSC 11059200L #define T1MS (65536-FOSC/12/1000)
#define 在C语言中我们经常翻译过来是“宏定义”,实际上这样的翻译可能造成误解,实际是替代、指代、起小名、起外号的意思。
张三的外号是二狗子,实际指代的是一个人。
11059200L 是张三,现在给这一串数字另起个名字二狗子 FOSC