实验内容
编写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)
这块照着这个图写就行了
5. 编程时需要注意的地方
- 往LCD写入指令代码或数据时,要通过一个下降沿,高电平与低电平之前要有一定的延时,这个延时不能太短(又是调了3个小时才发现的),比如我原来写的20个nop就不够,延时不够,指令代码或数据就写不进去,LCD就不显示。所需延时的长短可能和机器有关,可能你的机器上20个nop就足够了,代码中200个nop是一个比较稳妥的选择。
- 函数中变量声明必须写在开头,否则会报错!(也就是说你不能先执行一条语句再进行变量声明)(这点跟mysql存储过程的变量声明的规则是一样的)
- LCD初始化时必须要清屏,因为复位后显示RAM不会自动清0(当然如果你的显示RAM中本来就全是0,那你忘记清屏也看不到啥异常现象)
- ADC软件调0。我们可以通过减去空载时的ADC的结果来对ADC进行调0。(直接手写一个数字的话不太合适,因为空载时的ADC的结果变化挺大的,我的那台设备空载时就一会50多,一会90多)
- 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. 思考题
- 调零的原理,软件调零和机械调零的区别。
调零:称重台空载时,ADC的转换结果不是0,我们需要对ADC的结果进行修正使得空载时,最终结果是0.。
- 软件调零就是在程序中,在得到ADC转换结果后,减去一个偏移量,这个偏移量可以由空载时的ADC的转换结果来得到。
- 机械调零,则是调节称重传感器的滑动电阻,使得空载时ADC的转换结果为0
- 模/数和数/模的信号转换原理。
- 数模转换
数字量是用代码按数位组合起来表示的,对于有权码,每位代码都有一定的位权。为了将数字量转换成模拟量,必须将每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 主机再次发起一次通信,读取从机的数据;(主机发出读操作)
(来源)