这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 嵌入式开发 » MCU » 单片机C语言如何进行位操作

共2条 1/1 1 跳转至

单片机C语言如何进行位操作

工程师
2020-05-31 22:49:14     打赏

 由于PIC处理器对位操作是最高效的,所以把一些BOOL变量放在一个内存的位中,既可以达到运算速度快,又可以达到最大限度节省空间的目的。

  在C中的位操作有多种选择。

  *********************************************

  如:char x;x=x|0B00001000; /*对X的4位置1。*/

  char x;x=x&0B11011111; /*对X的5位清0。*/

  把上面的变成公式则是:

  #define bitset(var,bitno)(var |=1《《bitno)

  #define bitclr(var,bitno)(var &=~(1《《bitno))

  则上面的操作就是:char x;bitset(x,4)

  char x;bitclr(x,5)

  *************************************************

  但上述的方法有缺点,就是对每一位的含义不直观,最好是能在代码中能直观看出每一位代表的意思,这样就能提高编程效率,避免出错。

  如果我们想用X的0-2位分别表示温度、电压、电流的BOOL值可以如下:

  unsigned char x @ 0x20; /*象汇编那样把X变量定义到一个固定内存中。*/

  bit temperature@ (unsigned)&x*8+0; /*温度*/

  bit voltage@ (unsigned)&x*8+1; /*电压*/

  bit current@ (unsigned)&x*8+2; /*电流 */

  这样定义后X的位就有一个形象化的名字,不再是枯燥的1、2、3、4等数字了。

  可以对X全局修改,也可以对每一位进行操作:

  char=255;

  temperature=0;

  if(voltage)。..。..

  *****************************************************************

  还有一个方法是用C的struct结构来定义:

  如:

  struct cypok{

  temperature:1; /*温度*/

  voltage:1; /*电压*/

  current:1; /*电流*/

  none:4;

  }x @ 0x20;

  这样就可以用

  x.temperature=0;

  if(x.current)。..。

  等操作了。

  **********************************************************

  上面的方法在一些简单的设计中很有效,但对于复杂的设计中就比较吃力。如象在多路工业控制上。前端需要分别收集多路的多路信号,然后再设定控制多路的多路输出。如:有2路控制,每一路的前端信号有温度、电压、电流。后端控制有电机、喇叭、继电器、LED。如果用汇编来实现的话,是很头疼的事情,用C来实现是很轻松的事情,这里也涉及到一点C的内存管理(其实C的最大优点就是内存管理)。采用如下结构:

  union cypok{

  struct out{

  motor:1; /*电机*/

  relay:1; /*继电器*/

  speaker:1; /*喇叭*/

  led1:1; /*指示灯*/

  led2:1; /*指示灯*/

  }out;

  struct in{

  none:5;

  temperature:1; /*温度*/

  voltage:1; /*电压*/

  current:1; /*电流*/

  }in;

  char x;

  };

  union cypok an1;

  union cypok an2;

  上面的结构有什么好处呢?听小弟道来:

  细分了信号的路an1和an2;

  细分了每一路的信号的类型(是前端信号in还是后端信号out):

  an1.in ;

  an1.out;

  an2.in;

  an2.out;

  然后又细分了每一路信号的具体含义,如:

  an1.in.temperature;

  an1.out.motor;

  an2.in.voltage;

  an2.out.led2;等

  这样的结构很直观的在2个内存中就表示了2路信号。并且可以极其方便的扩充。

  如添加更多路的信号,只需要添加:

  union cypok an3;

  union cypok an4;

  。。。。。。。。。。。。。。。

  从上面就可以看出用C的巨大好处。

  PICC每日一贴。(初谈如何从汇编转向PICC)

  小弟不才,特抛砖引玉,与大家共勉。

  聊聊如何从汇编转向PICC。

  因为HIDE-TECH PICC破解版很多,所以HIDE PICC有比其它PICC有更多的用户,虽然它的编译效率不是最好。最好的是CCS,但没破戒版。。。,不过用HIDE PICC精心安排函数一样可以获得很高的编译效率,还是人脑是第一的。

  当然要求你要有C语言的基础。PICC不支持C++,这对于习惯了C++的朋友还得翻翻C语言的书。

  C代码的头文件一定要有

  #include《pic.h》

  它是很多头文件的集合,C编译器在pic.h中根据你的芯片自动栽入相应的其它头文件。

  这点比汇编好用。

  载入的头文件中其实是声明芯片的寄存器和一些函数。

  顺便摘抄一个片段:

  static volaTIle unsigned char TMR0 @ 0x01;

  staTIc volaTIle unsigned char PCL @ 0x02;

  staTIc volatile unsigned char STATUS @ 0x03;

  可以看出和汇编的头文件中定义寄存器是差不多的。如下:

  TMR0 EQU 0X01;

  PCL EQU 0X02;

  STATUS EQU 0X03;

  都是把无聊的地址定义为大家公认的名字。

  一:怎么附值?

  如对TMR0附值:

  汇编中:MOVLW 200;

  MOVWF TMR0;当然得保证当前页面在0,不然会出错。

  C语言:TMR0=200;//无论在任何页面都不会出错。

  可以看出来C是很直接了当的。并且最大好处是操作一个寄存器时候,不用考虑页面的问题。一切由C自动完成。

  二:怎么位操作?

  汇编中的位操作是很容易的。在C中更简单。

  C的头文件中已经对所有可能需要位操作的寄存器的每一位都有定义名称:

  如:PORTA的每一个I/O口定义为:RA0、RA1、RA2。。。RA7。

  OPTION的每一位定义为:PS0、PS1、PS2 、PSA 、T0SE、T0CS、INTEDG 、RBPU。

  可以对其直接进行运算和附值。

  如:

  RA0=0;

  RA2=1;

  在汇编中是:

  BCF PORTA,0;

  BSF PORTA,2;

  可以看出2者是大同小异的,只是C中不需要考虑页面的问题。

  三:内存分配问题:

  在汇编中定义一个内存是一件很小心的问题,要考虑太多的问题,稍微不注意就会出错。比如16位的运算等。用C就不需要考虑太多。

  下面给个例子:

  16位的除法(C代码):

  INT X=5000;

  INT Y=1000;

  INT Z=X/Y;

  而在汇编中则需要花太多精力。

  给一个小的C代码,用RA0控制一个LED闪烁:

  #include《pic.h》

  void main(){

  int x;

  CMCON=0B111; file://关掉A口比较器,要是有比较器功能的话。

  ADCON1=0B110; file://关掉A/D功能,要是有A/D功能的话。

  TRISA=0; file://A口全为输出。

  loop:RA0=!RA0;

  for(x=60000;--x;){;} file://延时

  goto loop;

  }

  说说RA0=!RA0的意思:PIC对PORT寄存器操作都是先读取----修改----写入。

  上句的含义是程序先读RA0,然后取反,最后把运算后的值重新写入RA0,这就实现了闪烁的功能。

  (一点经验)如何有效的实时控制LED闪烁。

  在很多设计中需要有精彩而实用的LED闪烁来表示设备工作正常与否和工作状态。

  在一些实时性要求不高的设计中可以用插入延时来控制LED闪烁。

  它的缺点现而易见:1:LED闪烁方式反映慢。2:在延时过程不能干其它工作(中断除外),浪费了资源。3:代码雍长,真正控制LED就几个个指令,其它的延时代码占了99%的空间。

  如果用TMR1或TMR2来做一个时钟,上面的种种缺点就可以避免,使得你可以腾出大量的时间做更有效的工作。

  下面是用TMR1作时钟的C代码(RB1、RB2、RB3控制LED)示例:

  void set_tmr1(){

  TMR1L=0xdc;

  TMR1H=0xb; /*设定初值3036*/

  T1CON=0B10001; /*设定TMR1 0.125s溢出一次*/

  }

  void interrupt time(){

  if(TMR1IF){

  T1CON=0B10000; /*关闭TMR1*/

  TMR1L=0xdc;

  TMR1H=0xb; /*TMR1设初值

  T1CON=0B10001; /*从新设分频比,打开TMR1*/

  if(s++》8){ /*每S清0*/

  s=0;

  if(ss++》60)/*每分钟清0*/

  ss=0;

  }

  TMR1IF=0;

  return;

  }

  }unsigned char s; /*每0.125S累加1*/

  unsigned char ss; /*每1秒累加1*/

  void main(){

  set_tmr1();

  。..。..。.; /*设定I/O口,开TMR1中断*/

  while(1){

  if(。..) /*判断闪烁方式语句,下同*/

  RB1=(bit)(s》4); /*每1s闪烁一次,占空比50%(调节》后面值可以改变)*/

  if(。..)

  RB2=(bit)(!ss); /*每1分钟闪烁一次,亮1秒,熄59秒*/

  if(。..)

  RB3=(bit)(s==0 || s==2 || s== 4 || s== 6); /*每0.25S闪烁一次*/

  。..。..。..; /*其它工作*/

  }

  }

  这样的框架对于基于要求实时性高的软件查询的程序是很有效的。

  在PICC中使用常数指针。

  常数指针使用非常灵活,可以给编程带来很多便利。

  我测试过,PICC也支持常数指针,并且也会自动分页,实在是一大喜事。

  定义一个指向8位RAM数据的常数指针(起始为0x00):

  #define DBYTE ((unsigned char volatile *) 0)

  定义一个指向16位RAM数据的常数指针(起始为0x00):

  #define CWORD ((unsigned int volatile *) 0)

  ((unsigned char volatile *) 0)中的0表示指向RAM区域的起始地址,可以灵活修改它。

  DBYTE[x]中的x表示偏移量。

  下面是一段代码1:

  char a1,a2,a3,a4;

  #define DBYTE ((unsigned char volatile *) 0)

  void main(void){

  long cc=0x89abcdef;

  a1=DBYTE[0x24];

  a2=DBYTE[0x25];

  a3=DBYTE[0x26];

  a4=DBYTE[0x27];

  while(1);

  }

  2:

  char a1,a2,a3,a4;

  #define DBYTE ((unsigned char volatile *) 0)

  void pp(char y){

  a1=DBYTE[y++];

  a2=DBYTE[y++];

  a3=DBYTE[y++];

  a4=DBYTE[y];

  }

  void main(void){

  long cc=0x89abcdef;

  char x;

  x=&cc;

  pp(x);

  while(1);

  }

  3:

  char a1,a2,a3,a4;

  #define DBYTE ((unsigned char volatile *) 0)

  void pp(char y){

  a1=DBYTE[y++];

  a2=DBYTE[y++];

  a3=DBYTE[y++];

  a4=DBYTE[y];

  }

  void main(void){

  bank1 static long cc=0x89abcdef;

  char x;

  x=&cc;

  pp(x);

  while(1);

  }

  关于BOOL量的一点应用。

  /*bit型变量只能是全局的或静态的,

  而有时我门在实际应用中既要改变某“位“变量的值;

  又要保证这个函数的独立性;那不可避免的要把

  这个函数做成有参函数,可是bit型变量是不能用做参数的;

  那该咋办泥?还好!有位段。

  看看:*/

  /********************************************/

  union FLAG

  {

  unsigned char BYTE;

  struct

  {

  unsigned char b0:1;

  unsigned char b1:1;

  unsigned char b2:1;

  unsigned char b3:1;

  unsigned char b4:1;

  unsigned char b5:1;

  unsigned char b6:1;

  unsigned char b7:1;

  }bool;

  };

  /********************************************/

  union FLAG mode;

  #define auto_bit mode.bool.b0

  #define cool_bit mode.bool.b1

  #define dar_bit mode.bool.b2

  #define fan_bit mode.bool.b3

  #define heat_bit mode.bool.b4

  #define swing_bit mode.bool.b5

  #define bed_bit mode.bool.b6

  #define time_bit mode.bool.b7

  /********************************************/

  void mode_task(in_mode)

  union FLAG *in_mode;

  {

  in_mode -》 bool.b0=1;

  in_mode -》 bool.b5=1;

  /*也可这样写

  in_mode -》 BYTE|=0x21;*/

  }

  /********************************************/

  void main(void)

  {

  mode.BYTE=0X00;

  while(1)

  {

  mode_task(&mode);

  }

  }

  /********************************************/

  这样写多爽!

  这里涉及了结构,联合,位段,及指针;可得先把基础概念搞清楚!

  用PICC写高效的位移操作。

  在许多模拟串行通信中需要用位移操作。

  以1-W总线的读字节为例,原厂的代码是:

  unsigned char read_byte(void)

  {

  unsigned char i;

  unsigned char value = 0;

  for (i = 0; i 《 8; i++)

  {

  if(read_bit()) value| = 0 x 01《《i;

  // reads byte in, one byte at a time and then

  // shifts it left

  delay(10); // wait for rest of timeslot

  }

  return(value);

  }

  虽然可以用,但编译后执行效率并不高效,这也是很多朋友认为C一定不能和汇编相比的认识提供了说法。

  其实完全可以深入了解C和汇编之间的关系,写出非常高效的C代码,既有C的便利,又有汇编的效率。

  首先对 for (i = 0; i 《 8; i++)做手术,改成递减的形式:

  for(i=8;i!=0;i--),因为CPU判断一个数是否是0(只需要一个指令),比判断一个数是多大来的快(需要3个指令)。

  再对value| = 0 x 01《《i;做手术。

  value| = 0 x 01《《i;其实是一个低水平的代码,效率低,DALLAS的工程师都是NO1,奇怪为什么会如此疏忽。

  仔细研究C语言的位移操作,可以发现C总是先把标志位清0,然后再把此位移入字节中,也就是说,当前移动进字节的位一定是0。

  那么,既然已经是0了,我们就只剩下一个步骤:判断总线状态是否是高来决定是否改写此位,而不需要判断总线是低的情况。

  于是改写如下代码:

  for(i=8;i!=0;i--){

  value》》=1; //先右移一位,value最高位一定是0

  if(read_bit()) value|=0x80; //判断总线状态,如果是高,就把value的最高位置1

  }

  这样一来,整个代码变得极其高效,编译后根本就是汇编级的代码。

  再举一个例子:

  在采集信号方面,经常是连续采集N次,最后求其平均值。

  一般的,无论是用汇编或C,在采集次数上都推荐用8,16,32、64、128、256等次数,因为这些数都比较特殊,对于MCU计算有很大好处。

  我们以128次采样为例:注:sampling()为外部采样函数。

  unsigned int total;

  unsigned char i,val;

  for(i=0;i《128;i++){

  total+=sampling();

  }

  val=total/128;

  以上代码是很多场合都可以看见的,但是效率并不怎么样,狂浪费资源。

  结合C和汇编的关系,再加上一些技巧,就可以写出天壤之别的汇编级的C代码出来

  首先分析128这个数是0B10000000,发现其第7位是1,其他低位全是0,那么就可以判断第7位的状态来判断是否到了128次采样次数

  在分析除以128的运算,上面的代码用了除法运算,浪费了N多资源,完全可以用右移的方法来代替之

  val=total/128等同于val=(unsigned char)(total》》7);

  再观察下去:total》》7还可以变通成(total《《1)》》8,先左移动一位,再右移动8位,不就成了右移7位了么?

  可知道位移1,4,8的操作只需要一个指令哦。

  有上面的概验了,就可以写出如下的代码:

  unsigned int total;

  unsigned char i=0

  unsigned char val;

  while(!(i&0x80)){ //判断i第7位,只需要一个指令。

  total+=sampling();

  i++;

  }

  val=(unsigned char)((total《《1)》》8); //几个指令就代替了几十个指令的除法运算

  哈哈,发现什么?代码量竟然可以减少一大半,运算速度可以提高几倍。

  再回头,就可以理解为什么采样次数要用推荐的一些特殊值了。




工程师
2020-06-09 22:38:43     打赏
2楼

学到了


共2条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]