前两篇教程中我们学习了LED、按键、开关的基本原理,数字输入输出的使用以及两者之间的关系。我们用到了 pin_mode 、 pin_read 和 pin_write 这三个函数,实际上它们离最底层(至少是单片机制造商允许我们接触到的最底层)就只有一步之遥了。而学单片机要是不了解一点底层,那跟Arduino玩家还有什么区别?(为防止有忠实的Arduino粉丝骂我,我得承认还是有一小部分Arduino玩家是知道本篇教程所介绍内容的。)根本不好意思说自己学过单片机好吧。这所谓的最底层,就是数字IO寄存器了。

在开始之前,你需要下载两份文档:

单片机的数据手册。官网链接极慢,我在国内平台上传了一份,在本篇教程写成之时是最新的。

开发板的原理图。本应在教程之初就放出来,但事实证明没有原理图也不影响使用。现在是肯定需要的。

等等,你可能还不知道寄存器是什么。那我们就从寄存器开始吧。

寄存器是一类CPU内部的存储器,分为通用寄存器与特殊功能寄存器(8086对特殊功能寄存器还有细分)。通用寄存器,顾名思义是通用的,可以存储操作数、运算结果、内存地址等数据,在使用C语言编程时,一般不直接接触通用寄存器,而由编译器负责安排其使用。特殊功能寄存器有特定的功能,一些作用于CPU内部,如PC存放下一条指令的地址,SP记录内存中栈顶的位置(现在无需了解这些);另一些与IO模组相连接,单片机程序通过这类寄存器来控制各种外设,我们今天要学习的数字信号IO寄存器就属于这一类,并且应该是其中最简单的了。

我们使用的单片机的型号是ATmega324PA,它有多种封装,引脚(pin)数不尽相同,但都有32个通用输入输出(GPIO)引脚。由于AVR架构是8位字长的,CPU一次处理1位数据和8位数据所需的时间是一样的,这32个引脚被组织为4个端口(port),分别是PA、PB、PC和PD。

在AVR架构tiny与mega系列的单片机中,每个端口都有3个寄存器控制数字信号IO,分别是PORTx、DDRx和PINx。这里的x是A、B、C或D,由于这4个端口在数字IO方面完全相同,就把它们合并起来讲。相应地,对于每个引脚Pxn,有PORTxn、DDxn(没有R)和PINxn三个bit控制其数字IO。

DDxn控制引脚方向:当DDxn为1时,Pxn为输出;当DDxn为0时,Pxn为输入。

当Pxn为输入时,如果PORTxn为1,则该引脚通过一个上拉电阻连接到VCC;否则引脚悬空。

当Pxn为输出时,如果PORTxn为1,引脚输出高电平;否则输出低电平。

PINxn的值为Pxn引脚的电平。如果给PINxn写入1,PORTxn的值会翻转。

还有很多细节问题,如MCUCR寄存器中PUD位的功能、复位后寄存器的值、输入输出切换的方法、读取引脚电平的延迟、未连接引脚的处理方法等,留作今天的作业,阅读数据手册I/O-Ports一章中除Alternate Port Functions一节以外的内容(一共8页不到,不多吧),找出这些问题的答案,并以此为基础回答上一篇教程最后的问题。

讲了这么多,相信你也没记住多少,而且你也不知道去哪里用这些寄存器。

要使用寄存器,你需要在C语言程序中写 #include <avr/io.h> (在创建项目时自动生成的代码中就有),然后就可以使用 PORTA 、 DDRB 、 PINC 等寄存器了。它们是宏定义,你不必去探究它们展开后是怎样的,只需知道这些宏可以读取,可以赋值,可以位操作,就像 uint8_t 类型变量一样。

但是诸如 PORTA0 和 DDB7 等宏定义却不代表寄存器上的那一位,它们实际上就是字面值常量,如 PORTAx 的意义是寄存器 PORTA 的第x位(第0位为最低位,第7位为最高位),它的值就是x。因此,直接对这些宏复制是不正确的(不仅意义不正确,编译也不会通过)。

在开发板的库函数中的 <ee1/bit.h> 提供了包含几个用于位操作的宏函数。我们先按照手册来用,稍后来看它们是如何实现的。

我们先返璞归真一下,回到最初的例子,点亮一个LED,不过这次我们不再使用 <ee1/led.h> 提供的函数,而是直接操作寄存器。

先点亮红色LED吧。在原理图的第2页左上角,红色LED通过一个电阻连接到网络LED0,而在第1页中LED0连接的是单片机PC4引脚,因此我们需要让PC4引脚输出高电平。回到上面看一下三个寄存器的功能,输出高电平需要DDxn和PORTxn同时为1。这里把x和n分别用C和4带入,即我们要让DDC4和PORTC4为1。

将一个寄存器的一位置为1可以由 set_bit 实现。它需要两个参数,要操作的整型变量与表示第几位的整数。把DDC4置为1应该写 set_bit(DDRC, ); ,4 可以用 DDC4 替换,这个定义就是这么用的。类似地也可以将PORTC4置为1。点亮红色LED的整个程序如下:

 #include <avr/io.h>
#include <ee1/bit.h> int main(void)
{
set_bit(DDRC , );
set_bit(PORTC, );
}

相信聪明的你已经知道闪烁和流水灯怎么写了。翻转输出电平可以使用 flip_bit(PORTC, ); ,也可以使用 set_bit(PINC, ); 。

下面来看数字输入。还是用第一个与按键相关的例子,让LED状态与按键保持一致,即按下亮起。

读取一个寄存器中的一位可以使用 read_bit。如果引脚上电平为高,read_bit 的运算结果非0(但不一定是布尔值1)。如果你没有忘记的话,按键按下时引脚电平为低,因此对读取引脚电平的结果取非才是按键是否按下。

在原理图中,按键一端连接在BTN0网络上,进而连接到单片机的PA4引脚。因此按键是否按下应该写为:!read_bit(PINA, ) 。

在读取之前应该先把引脚配置为输入。尽管复位后默认为输入,在这个例子中没有必要向DDA4写0,但明确写出来可以让看这段代码的人(可能别人也可能是你自己)明白PA4是作输入的,这样做是一种良好的习惯。至于PORTA4,由于这一引脚在外部有连接上拉电阻,就没有必要启用内部上拉电阻了。

 #include <avr/io.h>
#include <ee1/bit.h> int main(void)
{
reset_bit(DDRA, );
set_bit(DDRC, );
while ()
{
cond_bit(!read_bit(PINA, ), PORTC, );
}
}

再结合按键动作的知识,你应该知道怎样直接通过寄存器操作来判断按键动作了吧。

顺便说一句,以上两个程序都不必在项目属性中给linker加上libee1库。虽然代码中使用了 <ee1/bit.h> ,但其中都是宏定义,与linker无关。

之前留了一个问题,就是位操作是如何实现的。以下为 <ee1/bit.h> 中部分代码:

 #define set_bit  (r, b) ((r) |=  (1u << (b)))
#define reset_bit(r ,b) ((r) &= ~(1u << (b)))
#define read_bit (r, b) ((r) & (1u << (b)))
#define flip_bit (r, b) ((r) ^= (1u << (b)))

写那么多括号是为了防止出现运算符优先级的问题。假设r就是一个寄存器,比如PORTC,b就是一个数字,比如4,也可以是一个变量,那么 (r) |= (1u << (b)) 就相当于 r = r | 1u << b (后缀u表示无符号数,位操作的运算数一般都是无符号数)。对于二进制表示下的每一位,如果不是第b位,那么位或运算符右边此位为0,运算结果等于左边,即r的这些为保持不变;对于第b位,右边此位为1,无论左边此位的值是多少,结果一定是1,即这一位被置1;这样就实现了将一位置为1的功能。

reset_bit 的实现还要绕一个弯。1u << b 是一个第b位为1,其余位为0的数,那么 ~(1u << b) ,即位与赋值号右边,是一个第b位为0而其余为都是1的数。仿照上面的分析可得,运算结果的第b位一定是0而其余位与r中原来的值相同。类似的分析也可应用于 flip_bit :两个bit进行异或运算的结果,若相同则为1,不同则为0;当一个运算数是1时,结果就是另一个运算数取反;当一个运算数是0时,结果与另一个运算数相同;因此 flip_bit 就使r的第b为取反而其他为不变。

以上是向寄存器中的位写入的操作。用于读取位的 read_bit 的原理也大致相同,用寄存器的值与 1u << b 相与,仅当第b位为1时结果是 1u << b ,这是个非零数,否则结果为0。read_bit 语句可以直接放在 if 语句的条件部分,但如果是根据其结果决定一个变量是否加1,不能直接加上其运算结果,可以转换成 bool 类型或用 if 语句判断。

这篇教程有点长。好好消化一下,然后把以前写过的程序用寄存器重新写一遍,以此巩固所学的知识。

从本教程开始至今,我们先了解了LED灯、按键、拨动开关、数字输入输出的使用方法,然后学习C语言位操作与数字IO寄存器,终于打通了一条从底层到应用的路。而网络上很多教程都是反过来讲的,即先介绍寄存器,然后直接通过寄存器来驱动LED、检测按键等,甚至有直接写诸如 DDRB |= 0x0C; 或 if (PINB & 0x40) 这样的代码的,初学者怎么看得明白?站在我的角度,我觉得以上都是常识,都不用讲,尽管我学习的时候也颇费周折(正是因为那些反过来的教程)。现在我站在初学者的角度,认为本教程的讲解顺序是更容易理解的。

我学习计算机之前,总对计算机抱有特殊的幻想,觉得它什么都能干,很神奇。现在这些想法都没有了,尤其是在学习单片机的过程中。学习计算机教会我们分析问题、解决问题,而学习单片机让我们更好地理解计算机是如何按照我们的想法来解决问题的。这篇教程带你了解了寄存器,在你学习单片机的全过程中,它都会伴随着你。寄存器是硬件和软件之间的一个重要纽带,计算机的任何功能都离不开寄存器。CPU?有寄存器。总线通信?通过寄存器。内存分页?需要寄存器。万物基于寄存器。又有更多像寄存器一样的纽带,在电子空穴与丰富多彩的计算机世界之间建立起联系。它们看起来如此复杂,却又清晰明了,就算一夜之间所有计算机都突然消失,人类也能从电子管和打孔纸带开始,一层一层地构建起计算机的世界。而我们了解的只不过是这个巨大体系中的沧海一粟。

初入计算机世界,你想着计算机能干什么,学完计算机我能干什么。而计算机世界是如此高深,在逐渐深入后,你会明白计算机不能干什么,我不能干什么。数码管、蜂鸣器,它们一直在你的开发板上,你却不知道如何使用它们;更不用说那些高级到你没听说过的东西。学得越多,你会发现虽然原本不会的减少了,但脑海中萌生出的“不切实际”的想法更多——学习的速度永远赶不上认知的速度。本系列教程不能帮你消除所有“不会”,而是要在带你一步步消除一些“不会”的过程中让你学会发现更多“不会”并消除的方法。

AVR单片机教程——数字IO寄存器的更多相关文章

  1. AVR单片机教程——ADC

    ADC 计算机的世界是0和1的.单片机可以通过读取0和1来确定按键状态,也可以输出0和1来控制LED.即使是看起来不太0和1的PWM,好像可以输出0到5V之间的电压一样,达到0和1之间的效果,但本质上 ...

  2. AVR单片机教程——PWM调光

    本文隶属于AVR单片机教程系列.   PWM 两位数码管的驱动方式是动态扫描,每一位都只有50%的时间是亮的,我们称这个数值为其占空比.让引脚输出高电平点亮LED,占空比就是100%. 在驱动数码管时 ...

  3. AVR单片机教程——示波器

    本文隶属于AVR单片机教程系列.   在用DAC做了一个稍大的项目之后,我们来拿ADC开开刀.在本讲中,我们将了解0.96寸OLED屏,移植著名的U8g2库到我们的开发板上,学习在屏幕上画直线的算法, ...

  4. AVR单片机教程——DAC

    本文隶属于AVR单片机教程系列.   单片机的应用场景时常涉及到模拟信号.我们已经会使用ADC把模拟信号转换成数字信号,本讲中我们要学习使用DAC把数字信号转换成模拟信号.我们还将搭建一个简单的功率放 ...

  5. AVR单片机教程——走向高层

    本文隶属于AVR单片机教程系列.   在系列教程的最后一篇中,我将向你推荐3个可以深造的方向:RTOS.C++.事件驱动.掌握这些技术可以帮助你更快.更好地开发更大的项目. 本文涉及到许多概念性的内容 ...

  6. AVR单片机教程——旋转编码器

    好久没写这个系列了.今天讲讲旋转编码器. 旋转编码器好像不是单片机玩家很常用的器件,但是我们的开发板上有,原因如下: 旋转编码器挺好用的.电位器能旋转的角度有限,旋转编码器可以无限圈旋转:旋转时不连续 ...

  7. AVR单片机教程——数码管

    先解答之前一个思考题:如果不把引脚配置为输出而写高电平,连接LED会怎样? 实验结果是,LED会亮,但相比于输出高电平的情况,亮度很低.这是为什么呢? 通过上一篇教程我们知道,引脚输入输出模式是由寄存 ...

  8. AVR单片机教程——数字输出

    从上一篇教程中我们了解到,按键与开关的输入本质上就是数字信号的读取.这一篇教程要讲的是,控制LED的原理是数字信号的输出.数字IO是单片机编程之有别于桌面编程的各项内容中最简单.最基础的. 在讲数字信 ...

  9. AVR单片机教程——定时器中断

    本文隶属于AVR单片机教程系列.   中断,是单片机的精华. 中断基础 当一个事件发生时,CPU会停止当前执行的代码,转而处理这个事件,这就是一个中断.触发中断的事件成为中断源,处理事件的函数称为中断 ...

随机推荐

  1. 多线程编程学习六(Java 中的阻塞队列).

    介绍 阻塞队列(BlockingQueue)是指当队列满时,队列会阻塞插入元素的线程,直到队列不满:当队列空时,队列会阻塞获得元素的线程,直到队列变非空.阻塞队列就是生产者用来存放元素.消费者用来获取 ...

  2. 洛谷 P1514 【引水入城】

    题库 :洛谷 题号 :1514 题目 :引水入城 link :https://www.luogu.org/problemnew/show/P1514 思路 :搜索从第一排开始能覆盖最后一排的区间L ~ ...

  3. POJ-1213 How Many Tables( 并查集 )

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1213 Problem Description Today is Ignatius' birthday. ...

  4. HZNU Training 2 for Zhejiang Provincial Collegiate Programming Contest 2019

    赛后总结: T:今天下午参加了答辩比赛,没有给予队友很大的帮助.远程做题的时候发现队友在H上遇到了挫折,然后我就和她们说我看H吧,她们就开始做了另外两道题.今天一人一道题.最后我们在研究一道dp的时候 ...

  5. PAT L3-015. 球队“食物链”

    L3-015. 球队“食物链” 时间限制 1000 ms 内存限制 262144 kB 代码长度限制 8000 B 判题程序 Standard 作者 李文新(北京大学) 某国的足球联赛中有N支参赛球队 ...

  6. Python二元操作符

    def quiz_message(grade): outcome = 'failed' if grade<50 else 'passid' print ('grade', grade, 'out ...

  7. kubernetes ConfigMap和Secret:配置应用程序

    7.1.配置容器化应用程序 7.2.向容器传递命令行参数 7.2.1.待Docker中定义命令与参数 1.了解ENTRYPOINT与CMD ENTRYPOINT定义容器启动时被调用的可以执行程序 CM ...

  8. SpringBoot中获取微信用户信息从未如此简单!

    前言 不知道你是否参加过拼多多上邀请微信好友砍价功能,这个功能实现首先需要考虑的就是获取微信用户的信息.获取用户信息就是获取公众号下微信用户的信息,今天我就来讲讲如何从公众号下获取微信用户信息. 需要 ...

  9. android 拍照 相册 剪切以及显示功能

    一.概述 android的 图片拍照 ,相册选图,以及图片剪切功能可以说非常常用. 尤其是图片上传功能,必然用到此功能. 而公司最近的一个项目中正好用到该功能. 记录下来以便以后再次用到,直接拿来使用 ...

  10. Hive的动态分区

    关系型数据库(如Oracle)中,对分区表Insert数据时候,数据库自动会根据分区字段的值,将数据插入到相应的分区中,Hive中也提供了类似的机制,即动态分区(Dynamic Partition), ...