实验五 重量测量 C51实现

实验内容
编写C51程序,使用重量测量实验板测量标准砝码的重量,将结果(以克计)显示到液晶屏上。误差可允许的范围之间

有不懂或不对的地方->评论区留言

1.添加头文件

#include<reg51.h>
#include<intrins.h> 

reg51.h里面主要是一些特殊功能寄存器的地址声明,对可以位寻址的,还包括一些位地址的声明,如果如sfr P1=0x80; sfr IE=0xA8;sbit EA=0xAF等(来源

<intrins.h> 有一些汇编里的左移右移指令和

_nop_ 空操作(相当于8051 NOP 指令 )
_testbit_ 测试并清零位(相当于8051 JBC 指令)
_push_ 压栈
_pop_ 弹栈 (参考文章一参考文章二

2. 定义ADC相关的寄存器和中断允许位

sfr P1ASF = 0X9D;     //P1口模拟功能控制寄存器P1ASF
sfr ADC_CONTR = 0XBC; //ADC控制寄存器
sfr ADC_RES = 0XBD;   //A/D转换结果寄存器高
sfr ADC_RESL = 0XBE;  //A/D转换结果寄存器低
sfr AUXR1 = 0XA2;     //辅助寄存器1
bit EADC = 0XA8 ^ 5;  //A/D转换中断允许位。1允许0禁止
2.1 sfr是啥

sfr 用来定义8 位的特殊功能寄存器(来源

2.2 AUXR1是啥

AUXR1是辅助寄存器1
我们主要用它的第B2位,即ADRJ,来使10位A/D转换结果的最高2位放在ADC_RES寄存器的低2位,低8位放在ADC_RESL寄存器。(当然你也可以不用,那么默认是 :转换结果的高8位放在ADC_RES寄存器,低2位放在ADC_RESL寄存器)
在这里插入图片描述

3. 定义ADC控制寄存器操作常量

 #define ADC_POWER    	0X80    	//ADC电源控制位
 #define ADC_FLAG    	0X10    	//模数转换器(ADC)转换启动控制位
 #define ADC_START    	0X08		//ADC开始控制位
 #define ADC_SPEEDLL    0X00  		//超低速540 clocks
 #define ADC_SPEEDL    	0X20    	//低速360 clocks
 #define ADC_SPEEDH    	0X40		//高速180 clocks
 #define ADC_SPEEDHH    0X60 		//超高速90 clocks

4. 定义LCD引脚

#define LCD_DATA P2 //P2口为LCD的数据口
sbit RST = P1 ^ 5;  //复位引脚
sbit CS1 = P1 ^ 7;  //左半屏选择
sbit CS2 = P1 ^ 6;  //右半屏选择
sbit BUSY = P2 ^ 7; //BUSY位
sbit E = P3 ^ 3;    //使能引脚
sbit RW = P3 ^ 4;   //读/写选择器引脚(R/W)
sbit RS = P3 ^ 5;   //数据/命令选择器引脚(R/S)

sbit是对应可位寻址空间的一个位(参考文章一参考文章二

这块照着这个图写就行了
在这里插入图片描述

5. 编程时需要注意的地方

  1. 往LCD写入指令代码或数据时,要通过一个下降沿,高电平与低电平之前要有一定的延时,这个延时不能太短(又是调了3个小时才发现的),比如我原来写的20个nop就不够,延时不够,指令代码或数据就写不进去,LCD就不显示。所需延时的长短可能和机器有关,可能你的机器上20个nop就足够了,代码中200个nop是一个比较稳妥的选择。
  2. 函数中变量声明必须写在开头,否则会报错!(也就是说你不能先执行一条语句再进行变量声明)(这点跟mysql存储过程的变量声明的规则是一样的)
  3. LCD初始化时必须要清屏,因为复位后显示RAM不会自动清0(当然如果你的显示RAM中本来就全是0,那你忘记清屏也看不到啥异常现象)
  4. ADC软件调0。我们可以通过减去空载时的ADC的结果来对ADC进行调0。(直接手写一个数字的话不太合适,因为空载时的ADC的结果变化挺大的,我的那台设备空载时就一会50多,一会90多)
  5. ADC转换结果抖动会造成显示数字的不断变化,LCD屏幕上你的数字就会不断闪烁,为了消除这种闪烁,我们可以这么做:如果这次测量的结果与上一次相差不超过3则证明是ADC转换抖动造成的,不改变要显示的数据。否则就是真的换了重物,更新要显示的数据。(3是我根据我的机器抖动程度设的,你可根据情况设置合适的值)

6. 完整代码

#include <REG51.h>
#include <intrins.h>
#include <stdlib.h>
//<stdlib.h>里有取绝对值函数:abs
enum SCREEN_CHOICE
{
    
    
    LEFT,  //左半屏
    RIGHT, //右半屏
    WHOLE  //全屏
};
char DISPALY_PAGE = 2;
//定义ADC相关的寄存器和中断允许位
sfr P1ASF = 0X9D;     //P1口模拟功能控制寄存器P1ASF
sfr ADC_CONTR = 0XBC; //ADC控制寄存器
sfr ADC_RES = 0XBD;   //A/D转换结果寄存器高
sfr ADC_RESL = 0XBE;  //A/D转换结果寄存器低
sfr AUXR1 = 0XA2;     //辅助寄存器1
bit EADC = 0XA8 ^ 5;  //A/D转换中断允许位。1允许0禁止
//定义ADC控制寄存器操作常量
#define ADC_POWER 0X80   //ADC电源控制位
#define ADC_FLAG 0X10    //模数转换结束标志位,AD转换完后,ADC_FLAG=1,一定要软件清0。
#define ADC_START 0X08   //ADC开始控制位
#define ADC_SPEEDLL 0X00 //超低速540 clocks
#define ADC_SPEEDL 0X20  //低速360 clocks
#define ADC_SPEEDH 0X40  //高速180 clocks
#define ADC_SPEEDHH 0X60 //超高速90 clocks
//定义LCD引脚
#define LCD_DATA P2 //P2口为LCD的数据口
sbit RST = P1 ^ 5;  //复位引脚
sbit CS1 = P1 ^ 7;  //左半屏选择
sbit CS2 = P1 ^ 6;  //右半屏选择
sbit BUSY = P2 ^ 7; //BUSY位
sbit E = P3 ^ 3;    //使能引脚
sbit RW = P3 ^ 4;   //读/写选择器引脚(R/W)
sbit RS = P3 ^ 5;   //数据/命令选择器引脚(R/S)
//LCD 汉字字码 一个汉字32个字节,分别为“重“,”量”,“为”,“:”,"克"
char code zhong[] = {
    
    0x10, 0x10, 0x14, 0xD4, 0x54, 0x54, 0x54, 0xFC, 0x52, 0x52, 0x52, 0xD3, 0x12, 0x10, 0x10, 0x00, 0x40, 0x40, 0x50, 0x57, 0x55, 0x55, 0x55, 0x7F, 0x55, 0x55, 0x55, 0x57, 0x50, 0x40, 0x40, 0x00};
char code liang[] = {
    
    0x20, 0x20, 0x20, 0xBE, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xBE, 0x20, 0x20, 0x20, 0x00, 0x00, 0x80, 0x80, 0xAF, 0xAA, 0xAA, 0xAA, 0xFF, 0xAA, 0xAA, 0xAA, 0xAF, 0x80, 0x80, 0x00, 0x00};
char code wei[] = {
    
    0x00, 0x20, 0x22, 0x2C, 0x20, 0x20, 0xE0, 0x3F, 0x20, 0x20, 0x20, 0x20, 0xE0, 0x00, 0x00, 0x00, 0x80, 0x40, 0x20, 0x10, 0x08, 0x06, 0x01, 0x00, 0x01, 0x46, 0x80, 0x40, 0x3F, 0x00, 0x00, 0x00};
//colon是冒号的意思
char code colon[] = {
    
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
char code ke[] = {
    
    0x04, 0x04, 0xE4, 0x24, 0x24, 0x24, 0x24, 0x3F, 0x24, 0x24, 0x24, 0x24, 0xE4, 0x04, 0x04, 0x00, 0x80, 0x80, 0x43, 0x22, 0x12, 0x0E, 0x02, 0x02, 0x02, 0x7E, 0x82, 0x82, 0x83, 0x80, 0xE0, 0x00};
//LCD 数字字码 一个数字16个字节
char code NUMBER[10][16] = {
    
    {
    
    0x00, 0xE0, 0x10, 0x08, 0x08, 0x10, 0xE0, 0x00, 0x00, 0x0F, 0x10, 0x20, 0x20, 0x10, 0x0F, 0x00}, /*"0"*/
                            {
    
    0x00, 0x10, 0x10, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x20, 0x3F, 0x20, 0x20, 0x00, 0x00}, /*"1"*/
                            {
    
    0x00, 0x70, 0x08, 0x08, 0x08, 0x88, 0x70, 0x00, 0x00, 0x30, 0x28, 0x24, 0x22, 0x21, 0x30, 0x00}, /*"2"*/
                            {
    
    0x00, 0x30, 0x08, 0x88, 0x88, 0x48, 0x30, 0x00, 0x00, 0x18, 0x20, 0x20, 0x20, 0x11, 0x0E, 0x00}, /*"3"*/
                            {
    
    0x00, 0x00, 0xC0, 0x20, 0x10, 0xF8, 0x00, 0x00, 0x00, 0x07, 0x04, 0x24, 0x24, 0x3F, 0x24, 0x00}, /*"4"*/
                            {
    
    0x00, 0xF8, 0x08, 0x88, 0x88, 0x08, 0x08, 0x00, 0x00, 0x19, 0x21, 0x20, 0x20, 0x11, 0x0E, 0x00}, /*"5"*/
                            {
    
    0x00, 0xE0, 0x10, 0x88, 0x88, 0x18, 0x00, 0x00, 0x00, 0x0F, 0x11, 0x20, 0x20, 0x11, 0x0E, 0x00}, /*"6"*/
                            {
    
    0x00, 0x38, 0x08, 0x08, 0xC8, 0x38, 0x08, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00}, /*"7"*/
                            {
    
    0x00, 0x70, 0x88, 0x08, 0x08, 0x88, 0x70, 0x00, 0x00, 0x1C, 0x22, 0x21, 0x21, 0x22, 0x1C, 0x00}, /*"8"*/
                            {
    
    0x00, 0xE0, 0x10, 0x08, 0x08, 0x10, 0xE0, 0x00, 0x00, 0x00, 0x31, 0x22, 0x22, 0x11, 0x0F, 0x00},
                            /*"9"*/};
//函数声明
void delay_nops(int);
void turn_on_display();
int get_adc_result();
void display_result(int);
void init_adc();
void init_lcd();
void wait_until_not_busy();
void choose_screen(enum SCREEN_CHOICE screen);
void clear(enum SCREEN_CHOICE);
void set_page(char);
void set_col(char);

int bias = 0; //AD转换初始偏移

//ADC初始化函数
void init_adc()
{
    
    
    P1ASF = 0x01;                        //设置P1.1口当作模拟数据的输入口
    ADC_CONTR = ADC_POWER | ADC_SPEEDLL; //上电
    delay_nops(200);                     //初次打开内部A/D 转换模拟电源,需适当延时,等内部模拟电源稳定后,再启动A/D转换。
    AUXR1 = 0x04;                        //10位A/D转换结果的最高2位放在ADC_RES寄存器的低2位,低8位放在ADC_RESL寄存器
    EA = 1;                              //CPU开放中断
    EADC = 1;                            //允许A/D转换中断
}

//得到ADC转换结果
int get_adc_result()
{
    
    

    ADC_CONTR = ADC_POWER | ADC_SPEEDLL | ADC_START; //开始AD转换
    /*由于是2套时钟,所以,设置ADC_CONTR控制寄存器后,
		要加几个空操作延时才可以正确读到ADC_CONTR寄存器的值,
		原因是设置ADC_CONTR控制寄存器的语句执行后,
		要经过几个CPU 时钟的延时,其值才能够保证被设置进ADC_CONTR控制寄存器*/
    delay_nops(6); //经过几个时钟延时后,才能够正确读到ADC_CONTR控制寄存器的值
    while ((ADC_CONTR & ADC_FLAG) != 0x10)
    {
    
    
    }
    ADC_CONTR &= !ADC_FLAG; //AD转换完后,将ADC_FLAG清0?
    return (ADC_RES & 0x03) * 256 + ADC_RESL;
}
//修正ADC转换结果
int get_modified_result()
{
    
    
    int r = get_adc_result();
    int k = 3; //倍数,每个机器不同,需要自己试几次
    return (r - bias) / k;
}
//空载时adc的结果就是AD转换初始偏移
int get_bias()
{
    
    
    return get_adc_result();
}
//LCD初始化函数
void init_lcd()
{
    
    
    wait_until_not_busy();
    RST = 0; //复位
    delay_nops(200);
    RST = 1;
    delay_nops(200);
    turn_on_display();
    clear(WHOLE); //清屏 这个必须有!因为复位后显示RAM不会自动清0
}

//读取LCD状态字
void get_lcd_sw()
{
    
    
    RS = 0;
    RW = 1;
    E = 1;
}
//等待直到LCD非忙
void wait_until_not_busy()
{
    
    
    get_lcd_sw();
    while (BUSY)
    {
    
    
    }
}
//写控制lcd代码
void set_lcd(char control_code)
{
    
    
    wait_until_not_busy();
    RS = 0;
    RW = 0;
    LCD_DATA = control_code;
    //下跳沿写入
    E = 1;
    delay_nops(200);
    E = 0;
}
//写LCD数据
void lcd_data_write(char lcd_data)
{
    
    
    wait_until_not_busy();
    RS = 1;
    RW = 0;
    LCD_DATA = lcd_data;
    //下跳沿写入
    E = 1;
    delay_nops(200);
    E = 0;
}
//设置显示行
void set_row(char row)
{
    
    
    row = row & 0x3f; //Row范围0到63,高两位清零
    set_lcd(0xc0 + row);
}
//设置显示列
void set_col(char col)
{
    
    
    col = col & 0x3f; //Col范围0到63,高两位清零
    set_lcd(0x40 + col);
}
//设置显示页
void set_page(char page)
{
    
    
    page = page & 0x07; //Page范围0到7,取低三位
    set_lcd(0xb8 + page);
}
//开显示
void turn_on_display()
{
    
    
    set_lcd(0x3f);
}
//关显示
void turn_off_display()
{
    
    
    set_lcd(0x3e);
}
//选择屏
void choose_screen(enum SCREEN_CHOICE screen)
{
    
    
    switch (screen)
    {
    
    

    case LEFT:
        CS1 = 1;
        CS2 = 0;
        break; //左屏
    case RIGHT:
        CS1 = 0;
        CS2 = 1;
        break; //右屏
    case WHOLE:
        CS1 = 1;
        CS2 = 1;
        break; //全屏
    default:
        CS1 = 1;
        CS2 = 1;
        break; //全屏
    }
}
//清屏
void clear(enum SCREEN_CHOICE screen)
{
    
    
    int i, j;
    choose_screen(screen);
    for (i = 0; i < 8; i++)
    {
    
    
        set_page(i);
        set_col(0x00);
        /*注意这里的j是从0到63
			因为如果我们选择全屏的话,它也不是把整块屏幕算做一个屏,
			而是同时对左右两个半屏做相同的操作。
			所以最多能操作的列数还是64列。
			*/
        for (j = 0; j < 64; j++)
        {
    
    
            lcd_data_write(0x00);
        }
    }
}
//显示汉字
void display_Chinese(enum SCREEN_CHOICE screen, char page, char col, char *Chinese)
{
    
    
    //汉字是16*16,即16行,16列。一页有8行,所以需要两页
    int i = 0;
    choose_screen(screen);
    set_page(page);
    set_col(col);
    //汉字的上半部分
    for (i = 0; i < 16; i++)
    {
    
    
        lcd_data_write(Chinese[i]);
    }
    //汉字的下半部分
    set_page(page + 1);
    set_col(col);
    for (i = 16; i < 32; i++)
    {
    
    
        lcd_data_write(Chinese[i]);
    }
}
//显示数字
void display_number(enum SCREEN_CHOICE screen, char page, char col, int number)
{
    
    
    //数字是16*8,即16行,8列。一页有8行,所以需要两页
    int i = 0;
    choose_screen(screen);
    set_page(page);
    set_col(col);
    //数字的上半部分
    for (i = 0; i < 8; i++)
    {
    
    
        lcd_data_write(NUMBER[number][i]);
    }
    //数字的下半部分
    set_page(page + 1);
    set_col(col);
    for (i = 8; i < 16; i++)
    {
    
    
        lcd_data_write(NUMBER[number][i]);
    }
}
//显示重量测量结果
void display_result(int weight)
{
    
    
    int hundreds, tens, ones;
    hundreds = weight / 100;  //百位
    tens = weight % 100 / 10; //十位
    ones = weight % 10;       //个位

    display_Chinese(LEFT, DISPALY_PAGE, 0, zhong);
    display_Chinese(LEFT, DISPALY_PAGE, 1 * 16, liang);
    display_Chinese(LEFT, DISPALY_PAGE, 2 * 16, wei);
    display_Chinese(LEFT, DISPALY_PAGE, 3 * 16, colon);
    display_number(RIGHT, DISPALY_PAGE, 0, hundreds);
    display_number(RIGHT, DISPALY_PAGE, 1 * 8, tens);
    display_number(RIGHT, DISPALY_PAGE, 2 * 8, ones);
    display_Chinese(RIGHT, DISPALY_PAGE, 3 * 8, ke);
}
/* 对于延时很短的,要求在us级的,采用“_nop_”函数,这个函数相当汇编NOP指令,延时几微秒。
   NOP指令为单周期指令,可由晶振频率算出延时时间,对于12M晶振,12/12M=1uS。*/
void delay_nops(int n)
{
    
    
    while (n--)
    {
    
    
        _nop_();
    }
}

void main()
{
    
    
    int re1 = 0, re2 = 0, flag = 0, is_first = 1; //函数中变量声明必须写在开头,否则会报错
    init_adc();
    init_lcd();

    while (1)
    {
    
    
        if (is_first)
        {
    
    
            bias = get_bias();
            is_first = 0;
        }

        //下面的操作是为了消除ADC转换结果抖动造成的显示数字的不断变化
        //flag为0表示前一次测量的结果
        if (flag == 0)
        {
    
    
            re1 = get_modified_result();
            flag = 1;
        }
        //flag为1表示这次测量的结果
        else
        {
    
    
            re2 = get_modified_result();

            //如果这次测量的结果与上一次相差不超过3则证明是ADC转换抖动造成的,不改变要显示的数据
            if (abs(re2 - re1) < 3)
            {
    
    
            }
            else
            {
    
    
                //否则就是真的换了重物,更新要显示的数据,并更新flag标志
                re1 = re2;
                flag = 0;
            }
        }
        display_result(re1);
    }
}

7. 思考题

  1. 调零的原理,软件调零和机械调零的区别。
    调零:称重台空载时,ADC的转换结果不是0,我们需要对ADC的结果进行修正使得空载时,最终结果是0.。
  • 软件调零就是在程序中,在得到ADC转换结果后,减去一个偏移量,这个偏移量可以由空载时的ADC的转换结果来得到。
  • 机械调零,则是调节称重传感器的滑动电阻,使得空载时ADC的转换结果为0
  1. 模/数和数/模的信号转换原理。
  • 数模转换

数字量是用代码按数位组合起来表示的,对于有权码,每位代码都有一定的位权。为了将数字量转换成模拟量,必须将每1位的代码按其位权的大小转换成相应的模拟量,然后将这些模拟量相加,即可得到与数字量成正比的总模拟量,从而实现了数字—模拟转换
来源:百度百科

扫描二维码关注公众号,回复: 12421655 查看本文章
  • 模数转换

AD转换主要以下三种方法:逐次逼近法、双积分法、电压频率转换法 1)逐次逼近法

  • 逐次逼近式A/D是比较常见的一种A/D转换电路,转换的时间为微秒级。
    采用逐次逼近法的A/D转换器是由一个比较器、D/A转换器、缓冲寄存器及控制逻辑电路组成。 基本原理是从高位到低位逐位试探比较,好像用天平称物体,从重到轻逐级增减砝码进行试探。
    逐次逼近法的转换过程是:初始化时将逐次逼近寄存器各位清零;转换开始时,先将逐次逼近寄存器最高位置1,送入D/A转换器,经D/A转换后生成的模拟量送入比较器,称为
    Vo,与送入比较器的待转换的模拟量Vi进行比较,若Vo<Vi,该位1被保留,否则被清除。然后再置逐次逼近寄存器次高位为1,将寄存器中新的数字量送D/A转换器,输出的
    Vo再与Vi比较,若Vo<Vi,该位1被保留,否则被清除。重复此过程,直至逼近寄存器最低位。转换结束后,将逐次逼近寄存器中的数字量送入缓冲寄存器,得到数
    字量的输出。逐次逼近的操作过程是在一个控制电路的控制下进行的。 2)双积分法
    采用双积分法的A/D转换器由电子开关、积分器、比较器和控制逻辑等部件组成。如图所示。基本原理是将输入电压变换成与其平均值成正比的时间间隔,再把此时间间隔转换成数字量,属于间接转换。
  • 双积分法
    积分法A/D转换的过程是:先将开关接通待转换的模拟量Vi,Vi采样输入到积分器,积分器从零开始进行固定时间T的正向积分,时间T到后,开关再接通与Vi极性相反的基准电压VREF,将VREF输入到积分器,进行反向积分,直到输出为0V时停止积分。Vi越大,积分器输出电压越大,反向积分时间也越长。计数器在反向积分时间内所计的数值,就是输入模拟电压Vi所对应的数字量,实现了A/D转换。
  • 电压频率转换法
    采用电压频率转换法的A/D转换器,由计数器、控制门及一个具有恒定时间的时钟门控制信号组成,它的工作原理是V/F转换电路把输入的模拟电压转换成与模拟电压成正比的脉冲信号。电压频率转换法的工作过程是:当模拟电压Vi加到V/F的输入端,便产生频率F与Vi成正比的脉冲,在一定的时间内对该脉冲信号计数,时间到,统计到计数器的计数值正比于输入电压Vi,从而完成A/D转换。来源:ADC转换原理

3.I2C总线在信号通讯过程中的应用

在通信之初,主从机必须根据自己的要求约定好通信规则:command的定义和位置、address的位数和位置。

以读写从机寄存器数据为例:

假设从机寄存器地址为8位、从机寄存器也位8位(被读取数据为8位);

约定读command为0x01,写command位0x02;

约定主机发起通信后,第一个slave address字节收到ack后,紧跟的一个字节为command,再下面一个字节为address。

1. 读寄存器数据步骤:

1.1 主机先发起一次通信,将读command(0x01)和需要读取的寄存器地址address写入从机;(主机发出写操作)

1.2 从机firmware的处理:

1.2.1 将command和address分别提取出来;

1.2.2 判断command的含义(本例中,是读指令还是写指令);

1.2.3 根据收到的的address,将对应寄存器的的数据放入从机I2C输出buffer;(这个步骤可以使用指针)

1.3 主机再次发起一次通信,读取从机的数据;(主机发出读操作)
来源

猜你喜欢

转载自blog.csdn.net/weixin_42378324/article/details/108666257
C51