AVR单片机的端口位操作方法解析:位域bit field(和STM32位带bit-band不同,位带是利用位域的功能做成的)方式和移位宏方式,文中有位域和移位宏定义的例程反汇编,便于理解他们的实现机制

本文详细解析了AVR单片机端口位操作的传统方法,如宏定义,以及如何利用C语言的位运算和指针创建更高效、通用的位操作头文件。通过实例演示和比较,展示了使用位域和移位宏的不同效果,适用于ATMega系列MCU。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

AVR单片机的端口位操作方法解析

来源:未知•作者:工程师周亮• 2018年11月22日 16:07 • 3708次阅读

正在上传…重新上传取消​ 0

一、常规方法

AVR单片机的各类教材或编程应用参考资料,对介绍的端口位操作方法不外乎宏定义及整体和某个常量相或、相与来实现某一单个位状态的改变。如:

PORT&=-(1《1);等价于:PORTB&=oxfd;作用是PB1清零而其余位不变。与之类似的还有:

PORTBl=(1《4);PB4置位,其余位不变。

PORTB=(k《4);PB4翻转,其余位不变。

上述方法,无论是常量值参与还是移位操作,用起来总嫌麻烦,不够直观,且具体常量值还需人工推算,易出错。移位操作生成的目标代码偏大,在大量运用时,占用系统内存或flash空间,执行效率低。

以实际运用效果来看,宏定义比较好。也偏好运用宏定义。C编译器在编译之前会事先进行宏替换,所以,如果宏定义讲究一些技巧,则代码执行效率将得到较大提升,编程时,操作也得心应手。

常规参考手册或资料,建议用以下宏定义:

AVR单片机的端口位操作方法解析

上图最后几行操作因为水印看不清楚可以参考下面的内容

                  #define Set_Bit(val, bitn)    (val |=(1<<(bitn)))
                 #define Clr_Bit(val, bitn)     (val&=~(1<<(bitn)))
                #define Get_Bit(val, bitn)    (val &(1<<(bitn)) )
         三个分别用来设置某一位,清除某一位,取某一位的值.
           使用方法为.Set_Bit(PORTA,3);   Clr_Bit(PORTB,2);   Get_Bit(val,5);

根据最开始的第一张图片再加一个位取反的宏定义 

 #define Cpl_Bit(val, bitn)    (val ^=(1<<(bitn)))              2021.10.15

如此宏定义之后,可构成一个头文件,然后加入到新建工程文件中(编译器用ICC的ICC6.31A),但之前需包含相应单片机的头文件,如:MCU为AT-MEGA48,则最先包含iom48V.h之后,再包含此自制头文件,即可在程序中运用:

AVR单片机的端口位操作方法解析

此法在运用时,依旧用了移位操作,只是为了方便程序操作,只在小规模程序中运用。

下面,将充分利用C语言自身的强大位运算能力和指针相结合,构建一个ICC6.31A平台下的位定义头文件,希望能给大家一些启示。

首先明确几个基本概念:位域、地址绑定,和VolaTIle限定关键字

在ICC6.31A的安装目录中有个in-clude文件夹,里面有大量编译器开发人员已为我们开发好的各类AVR单片机预定头文件,下面是iom48v.h头文件。

AVR单片机的端口位操作方法解析

AVR单片机的端口位操作方法解析

注:每个端口均有PIN、DDR、PORT三个八位寄存器

简要概括如下:

VolaTIle -词用来规定C编译器不允许对其限定的变量进行优化处理。如:

AVR单片机的端口位操作方法解析

这句宏定义要结合iom48v.h头文件来看,在前面有该头文件关于各端口寄存器的定义。上述宏语句中,ox25被强制转化为一个指针常量,实际上,上述宏定义的意义为:PORTB被强制定义在地址ox25上,即定义了一个无符号字符型变量PORTB,且被强制绑定在ox25地址上。其他语句依此类推。

有了这个概念之后,再来了解一下C语言中位域的定义。标准c语言中,可以定义一个特殊的结构,位域,允许对定义的结构中的单个位进行操作。基本构成如下:

AVR单片机的端口位操作方法解析

下面给出的位域定义等的头文件,在自定义位操作头文件中定义了一个位域BYTE_BIT。

自定义avt_bit.h头文件(节选,以ATMEGA48为例,定义其B口)

AVR单片机的端口位操作方法解析

对各端口依同一规律均进行各位的定义即可。

结合头文件的相关定义,重新对其特定地址进行另外的绑定,结合位域的概念.进而一步步将各寄存器由一个字节分成了可操控的8个位。在上述文件中,仅表述了B端口。另外,Atmega48的端口不完全,只有B、C、D口,没有A口,且D口为8位,C口为6位.B口为7位,若外接晶体,则PB6和PB7不能另作端口运用。所以,端口很不完整,但为了保持位域完整性和一致性,便于理解,将B、C、D三口均作8bit对待。实际操作时,千万注意不要去操作那些实际不存在的位。当然,若感兴趣,大家可改动头文件相关定义,来完善它。

为验证这个头文件,很快编写了一个测试程序Beep.e,具体如下:

AVR单片机的端口位操作方法解析

在电路拓扑中,在ATMEGA48的PBO脚,即(14)脚外接一只三极管(接b极)由三极管去控制一只蜂鸣器。程序很简单。为了更直观,在PORTB口其他不用的引脚上均接了发光二极管,此时,在测试程序中,将端口初始化函数语句改为:

DDRB=oxff;PORTB=ox00;(或PORTB=oxff;视二极管接法而定),由二极管配合蜂鸣器来观察PBO位是否能单独动作。当然,运用此位定义头文件,可实现单片机任一口的任一位的位操作。

二、扩展运用

基于上述原理,结合C语言取地址运算符&,不难实现一个通用位定义头文件,来适合所有AVR单片机,只是代码较为复杂。大家不妨自己试试。

注意一点:以上头文件,是参考ICC6.31A编译器的include文件夹定义的,在使用前,请首先包含系统提供的MCU头文件,并将自定义头文件复制到所建工程中,然后,就可运用自如了。

就目前使用情况来看,使用AT-MEGA48/16/128的MCU较多,所以即便是定义3个对应的位操作头文件,也是可以的。通用型文件往往体积较大,编制麻烦,可读性差,不适合普通爱好者。

 AVR单片机的端口位操作方法解析 - 控制/MCU - 电子发烧友网AVR单片机的端口位操作方法解析-如此宏定义之后,可构成一个头文件,然后加入到新建工程文件中(编译器用ICC的ICC6.31A),但之前需包含相应单片机的头文件,如:MCU为AT-MEGA48,则最先包含iom48V.h之后,再包含此自制头文件,即可在程序中运用:http://www.elecfans.com/emb/danpianji/20181122820061.html

AVR的两种位操作的比较(位域方式和移位宏方式)

测试环境如下:
硬件:AT90S2313
软件:    WiinAVR gcc3.3   -Os级优化(最小size)


说明:
    由于AVR不支持位操作,所以必须通过软件来实现。下面对我所知道的两种方法进行一个简单的比较。
    1、位域方式。先定义一个位域,
            typedef struct _bit_struct
            {
                unsigned char bit0 : 1 ;
                unsigned char bit1 : 1 ;
                unsigned char bit2 : 1 ;
                unsigned char bit3 : 1 ;
                unsigned char bit4 : 1 ;
                unsigned char bit5 : 1 ;
                unsigned char bit7 : 1 ;
                unsigned char bit6 : 1 ;
            }bit_field;
        
再用一个宏    ,来指向要操作的位。
             #define LED             GET_BITFIELD(PORTB).bit0
             #define BUTTON      GET_BITFIELD(PINB).bit7
        使用时只需要直接赋值即可:如LED =     0 ,LED = 1,  或者直接判断 LED==0    ,    LED ==1.
        这种方法类似C51中的位操作。直接。

    2、位移宏方式。主要有三个.
                #define Set_Bit(val, bitn)    (val |=(1<<(bitn)))
                #define Clr_Bit(val, bitn)     (val&=~(1<<(bitn)))
                #define Get_Bit(val, bitn)    (val &(1<<(bitn)) )
         三个分别用来设置某一位,清除某一位,取某一位的值.
           使用方法为.Set_Bit(PORTA,3);   Clr_Bit(PORTB,2);   Get_Bit(val,5);

根据最开始的第一张图片再加一个位取反的宏定义  #define Cpl_Bit(val, bitn)    (val ^=(1<<(bitn)))

2021.10.15    cpl取反指令的全称是三个字母首字母简写Converse Position Logical,
    3、测试程序.
           
说明,假设PORTB.7接按纽,PORTB.0 LED
           
测试程序完成如下操作。
                   BUTTON == 0 ,LED输出否则输出0
                   这样的目的是即测试了输入,又测试了输出1和输出0,相对全面一点。  C代码如下.

                    // testled.c     
测试AVR的位操作.
                // 
这是gcc;如是其它编译器,请修改。
                #include <avr/io.h>

                // 
定义一个寄存器(Register)或端口(Port)的八个位
                typedef struct _bit_struct
                {
                    unsigned char bit0 : 1 ;
                    unsigned char bit1 : 1 ;
                    unsigned char bit2 : 1 ;
                    unsigned char bit3 : 1 ;
                    unsigned char bit4 : 1 ;
                    unsigned char bit5 : 1 ;
                    unsigned char bit7 : 1 ;
                    unsigned char bit6 : 1 ;
                }
bit_field;

                  //定义一个宏,用来得到每一位的值
                #define GET_BITFIELD(addr) (*((volatile  bit_field *) (addr)))

                //定义每一个位
                #define LED             GET_BITFIELD(PORTB).bit0
                #define BUTTON      GET_BITFIELD(PINB).bit7



                #define Set_Bit(val, bitn)    (val |=(1<<(bitn)))
                #define Clr_Bit(val, bitn)     (val&=~(1<<(bitn)))
                #define Get_Bit(val, bitn)    (val &(1<<(bitn)) )

                int main( void )
                {
                    DDRB = 0x41;   //
配置PB0为输出,PB7为输入
                    if ( BUTTON==0 )     LED = 1; else LED = 0;
                    //if(!Get_Bit(PINB,7) ) 
 Set_Bit(PORTB,0);    else Clr_Bit(PORTB,0);//宏定义后面做语句也要加分号2021.10.16
                    while(1);
                }
                //     ----------------------        end         -----------------------------
    4
、测试过程。
       a.先使用位域方式。
       主程序中使用 if ( BUTTON==0 )     LED = 1; else LED = 0;
       
结果如下:
                     int main( void )
                    {
                      4a:    cf ed           ldi    r28, 0xDF    ; 223
                      4c:    d0 e0           ldi    r29, 0x00    ; 0
                      4e:    de bf           out    0x3e, r29    ; 62
                      50:    cd bf           out    0x3d, r28    ; 61
                        DDRB = 0x41;      //
配置PB0为输出,PB7为输入
                      52:    81 e4           ldi    r24, 0x41    ; 65
                      54:    87 bb           out    0x17, r24    ; 23
                        if ( BUTTON==0 )     LED = 1; else LED = 0;
                      56:    86 b3           in    r24, 0x16    ; 22
                      58:    e8 2f           mov    r30, r24
                      5a:    ff 27           eor    r31, r31
                      5c:    80 81           ld    r24, Z
                      5e:    86 fd           sbrc    r24, 6
                      60:    07 c0           rjmp    .+14         ; 0x70
                      62:    88 b3           in    r24, 0x18    ; 24
                      64:    e8 2f           mov    r30, r24
                      66:    ff 27           eor    r31, r31
                      68:    80 81           ld    r24, Z
                      6a:    81 60           ori    r24, 0x01    ; 1
                      6c:    80 83           st    Z, r24
                      6e:    06 c0           rjmp    .+12         ; 0x7c
                      70:    88 b3           in    r24, 0x18    ; 24
                      72:    e8 2f           mov    r30, r24
                      74:    ff 27           eor    r31, r31
                      76:    80 81           ld    r24, Z
                      78:    8e 7f           andi    r24, 0xFE    ; 254
                      7a:    80 83           st    Z, r24
                        while(1);
                      7c:    ff cf           rjmp    .-2          ; 0x7c

         main
函数共52Bytes.其中,从lst文件看得出:main函数的初始化用了4指令8Bytes. 最后一句while(1);用了1条指令2Bytes.( for循环和do-while也是)
         DDRB=0x41
用了2条指令4Bytes. 计算一下:52-8-4-2=38Bytes,if ( BUTTON==0 )     LED = 1; else LED = 0; 这句用了19条指令38Bytes. (居然运用了3个寄存器白r24,r30,r31,和一个Z,代码真是苦涩,,我看不懂,准备以后作代码加密用

.  )
       b.
使用移位宏方式。
        if ( BUTTON==0 )     LED = 1; else LED = 0;  换为等效的     if(!Get_Bit(PINB,7) )  Set_Bit(PORTB,0);    else Clr_Bit(PORTB,0);

       
结果,main函数仅24Bytes.其它代码一样,略去所以,上面这句代码仅用了24-14=10Bytes ,5条指令。生成的代码如下:
              56:    b7 99           sbic    0x16, 7    ; 22
              58:    02 c0           rjmp    .+4          ; 0x5e
              5a:    c0 9a           sbi    0x18, 0    ; 24
              5c:    01 c0           rjmp    .+2          ; 0x60
              5e:    c0 98           cbi    0x18, 0    ; 24
    5. 
菜论:鱼和熊掌。
      由于AVR可以I/O脚进行sbic,sbi,cbi,这样的位操作,所以使用I/O脚操作时,移位宏可以产生高效的代码。
      例如,要实现上面的几个简单的指令,为了实现LED=1这样的类似C51的sbit的效果,我必须多付出(38-10=28Bytes)的代价。
    6......
        对于I/O脚,可以产生这样高效的代码,是因为有sbi和cbi这样的指令(但是这两指令仅限于地址在00-1F之间的地址),那么对于一般的变量,又如何呢?. 

AVR的两种位操作及比较 - 单片机/MCU论坛 - 电子技术论坛 - 广受欢迎的专业电子论坛!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值