51 开源 电容表: 8H8K64U比较器+OLED12864-I2C 实现
51 开源 电容表:
8H8K64U比较器+OLED12864-I2C 实现,最大量程25mF
电路简单,工作可靠,读数稳定,测量的电容量就是在普通的振荡电路中的容量。
赶工做出来的,测试有问题可以反馈给我。
两个键,短按放开为【档位+】、【档位-】,长按任一键超过1秒读数归0并保存。
测电容方法很多,不同的方法、不同的测试电压、测试频率其容量会有差别。
本电容表测试几十pF、几百pF的电容误差比较大,可能到5%,
超过1nF的误差一般2%以下。
分别为测量330nF、100pF、330pF、22nF、330nF(再重复测一次)
分别为测量10uF、100uF。
分别为测量470uF、3300uF、10000uF
电路原理图PDF、PCB、程序C源码:
下载开源程序包解压缩后如下:
【CAN 原理及实战,8课时】
线上视频授课:10月/30号和11月/1号:14:00 ~ 17:00;
腾讯会议号:885-5858-2739; (安装腾讯会议软件后,输入会议号即可)
参会学习立即【免费+包邮送】USB转2组独立的CAN核心功能实验板
模拟的CAN收发器您自己补上
https://www.stcaimcu.com/forum.php?mod=viewthread&tid=4526&extra=&page=1
请帮忙转发给可能需要:从0开始了解CAN的 同学/同事/老师/研发人员
//main-电容表充放电版-V2E.c 中的程序
/* ---技术交流: www.STCAIMCU.com ------------------------------------*/
/* ---资料下载: www.STCAI.com -----------------------------------------*/
/* --- Tel: 86-0513-55012928,55012929,55012966 ---------------------*/
/* If you want to use the program or the program referenced in the article, */
please specify in which data and procedures from STC */
#include "config.h"
#include "OLED128x64.h"
#include "EEPROM.h"
/**************************************************
main-电容表充放电版-V2E.c增加EEPROM保存0点读书。
用STC8H8K64U-TSSOP20的比较器做的电容表,5档量程,用OLED显示。
电阻 范围 放电(2欧姆)
1M 0.001 nF ~ 2.5 uF 1ms
100K 0.01 nF ~ 25 uF 1ms
10K 0.1 nF ~ 250 uF 4ms
1K 1 nF ~2500 uF 40ms
100R 0.01 uF ~ 25000 uF 400ms
P3.0--按键2, 准双向口,低有效。档位+/归0。
P3.2--按键1, 准双向口,低有效。档位-/归0。
P3.3--Timer1门控输入+INT1外部中断,准双向口,高电平计数, 低电平停止计数,下降沿中断。
P3.4--模拟比较器输出,推挽输出。
P3.5--放电控制,推挽输出,高有效。
P3.6--模拟比较器-输入, 高阻.
P3.7--模拟比较器+输入, 高阻.
P1.0--OLED12864-SCL 开漏。
P1.1--OLED12864-SDA 开漏。
P1.3--RELAY1,推挽输出,高有效。 100R, 20mF档( 0.01uF~20.00mF)。
P5.4--RELAY2,推挽输出,高有效。 1K, 2mF档(0.001uF~2.000mF)。
P1.4--RELAY3,推挽输出,高有效。 10K, 200uF档(0.1nF~200.0uF)。
P1.5--RELAY4,推挽输出,高有效。 100K, 20uF档( 0.01nF~20.00uF)。RELAY1~4均断开则是2uF档(0.001nF~2.000uF).
P1.6 P1.7--24MHz晶振。高阻.
Timer1工作于12T模式, 测量充电时间, 电容 C = t/R.
**************************************************/
/********************* IO、变量、常量定义 *******************************/
sbit P_KEY1 = P3^2; //按键1, 准双向口,低有效。
sbit P_KEY2 = P3^0; //按键1, 准双向口,低有效。
sbit P_RELAY1 = P1^3; //RELAY1,推挽输出,高有效。 100R, 20mF档( 0.01uF~20.00mF)。
sbit P_RELAY2 = P5^4; //RELAY2,推挽输出,高有效。 1K, 2mF档(0.001uF~2.000mF)。
sbit P_RELAY3 = P1^4; //RELAY3,推挽输出,高有效。 10K, 200uF档(0.1nF~200.0uF)。
sbit P_RELAY4 = P1^5; //RELAY4,推挽输出,高有效。 100K, 20uF档( 0.01nF~20.00uF)。RELAY1~4均断开则是2uF档(0.001nF~2.000uF).
sbit P_DISCHARGE = P3^5; //放电控制,推挽输出,高有效。
#define K_LEVEL_UP 1
#define K_LEVEL_DOWN 2
#define K_ZERO 3
u8 key_state, KeyCode, KeyHoldCnt;
bit B_end;
bit B_neg;
bit B_zero;
u8 cnt_50ms;
u8 level; //档位: 0:2uF, 1:20uF, 2:200uF, 3:2mF, 4:20mF
u32 C_zero;
u16 DischargeTimeCal;
u16 DischargeTime;
u8 CapCntH;
u8 index;
u16 LimitTime;
u16 UpdateTime;
u16 ChargeCnt;
u8 DotState; //小数点位置: 0:不显示小数点,1:第一位后,2:第二位后,3:第三位后
u8 xdata tmp;
/********************* 函数声明 *******************************/
void DEC_x_xxx(u16 j);
void DEC_xx_xx(u16 j);
void DEC_xxx_x(u16 j);
void DEC_xxxx(u16 j);
void Timer1_config(void);
void Compare_Config(void); //比较器初始化
void ClearCapacitance(void); //清除电容显示值
void ShowLevel(void); // 显示档位
u16 MODBUS_CRC16(u8 *p, u8 n);
// 2uF 20uF 200uF 2mF 20mF
float code T_C_Sale={ 1.0321f, 1.0091f, 1.0098f, 1.0060f, 1.0125f}; // 电容修正系数, 根据实际测试情况调整
/*************************** 主程序 *************************************/
void main(void)
{
u8 i;
u16 crc;
u32 j;
P_SW2 = 0x80;
XOSCCR = 0xc0; //启动外部晶振
while (!(XOSCCR & 1)); //等待时钟稳定
CLKDIV = 0x00; //时钟不分频
CKSEL = 0x01; //选择外部晶振
P_KEY1 = 1; //按键1, 准双向口,低有效。
P_KEY2 = 1; //按键1, 准双向口,低有效。
P_RELAY1 = 0; //RELAY1,推挽输出,高有效。 100R, 20mF档( 0.01uF~20.00mF)。
P_RELAY2 = 0; //RELAY2,推挽输出,高有效。 1K, 2mF档(0.001uF~2.000mF)。
P_RELAY3 = 0; //RELAY3,推挽输出,高有效。 10K, 200uF档(0.1nF~200.0uF)。
P_RELAY4 = 0; //RELAY4,推挽输出,高有效。 100K, 20uF档( 0.01nF~20.00uF)。RELAY1~4均断开则是2uF档(0.001nF~2.000uF).
P_DISCHARGE = 1; //放电控制,推挽输出,高有效。
P3n_standard(0x0f); //P3.0~P3.3设置为准双向口
P1n_pure_input(0xc0); //设置P1.7 P1.6为高阻输入,晶振
P3n_push_pull(Pin5); //放电控制, 推挽输出,高有效。
P1n_push_pull(0x38); //继电器驱动, 推挽输出,高有效。
P5n_push_pull(0x10); //继电器驱动, 推挽输出,高有效。
OLED_config();
printf_ascii_10x24(40,5,"");
FillAll(0);
printf_ascii_6x8(0, 0, "-------C METER-------");
level = 2; //上电默认档位,档位: 档位: 0:2uF, 1:20uF, 2:200uF, 3:2mF, 4:20mF
ShowLevel(); //显示档位
Compare_Config();
Timer1_config();
EA = 1; //打开总中断
EEPROM_read_n(0,tmp,22); //读EEPROM
if(MODBUS_CRC16(tmp,22) == 0) //记录正确
{
C_zero = ((u32 *)&tmp); // 2uF
C_zero = ((u32 *)&tmp); //20uF
C_zero = ((u32 *)&tmp); // 200uF
C_zero = ((u32 *)&tmp); // 2mF
C_zero = ((u32 *)&tmp); //20mF
if(C_zero >= 1000) C_zero = 200; //0点异常
if(C_zero >= 1000) C_zero = 20; //0点异常
if(C_zero >= 1000) C_zero = 2; //0点异常
if(C_zero >= 1000) C_zero = 1; //0点异常
if(C_zero >= 1000) C_zero = 1; //0点异常
}
else //EEPROM记录不正确,则默认0点
{
C_zero = 200; // 2uF 放电管MOSFET截止时DS极有超过200pF电容,所以会有一个200多的读数。
C_zero = 20; //20uF
C_zero = 2; // 200uF
C_zero = 1; // 2mF
C_zero = 1; //20mF
}
DotState = 5; //给一个非法值,第一次显示就会给定正确值
while(1) //1ms节拍
{
OLED_delay_ms(1);
if(++cnt_50ms == 50)
{
cnt_50ms = 0;
i = key_state;
B0 = P_KEY1;
B1 = P_KEY2;
key_state = ~B & 0x03;
B = (i ^ key_state) & i; //键释放
if((B != 0) && (KeyHoldCnt < 16))
{
if(B1) KeyCode = K_LEVEL_UP; //档位变大
if(B0) KeyCode = K_LEVEL_DOWN; //档位变小
}
if(key_state != 0) //键按着
{
if(++KeyHoldCnt == 100) KeyHoldCnt = 99;
if(KeyHoldCnt == 16) KeyCode = K_ZERO;
}
else KeyHoldCnt = 0;
if(KeyCode != 0)
{
if(KeyCode == K_ZERO) B_zero = 1; //长按1秒归0
else if(KeyCode == K_LEVEL_UP) //档位变大
{
if(++level >= 5) level = 0;
ShowLevel(); //显示档位
}
else if(KeyCode == K_LEVEL_DOWN) //档位变小
{
if(--level >= 5) level = 4;
ShowLevel(); //显示档位
}
KeyCode = 0;
}
}
if(LimitTime != 0) LimitTime--; //限时时间倒计时
if(UpdateTime != 0) UpdateTime--; //更新时间倒计时
if(index == 0) //启动
{
TR1 = 0;
CapCntH = 0;
TH1 = 0;
TL1 = 0;
LimitTime= 2500; //限时2500ms
UpdateTime = 500; //500ms更新一次, 超过按实际时间更新
ChargeCnt= 0; //充电次数计数
index = 1; //下一步
}
else if(index == 1) //开始放电
{
P_DISCHARGE = 1; //开始放电
if(level <= 1) DischargeTime = 1; //2uF 20uF档放电时间1ms, 放电电阻2R.
else if(level == 2) DischargeTime = 1 + DischargeTimeCal; //200uF档放电时间1ms+修正时间(100uF增加1.6ms, 即8倍RC值), 根据充电时间来修正, 充电时间越长电容越大则放电时间也越长, 保证放点时间为6*2*C以上.
else if(level == 3) DischargeTime = 10 + DischargeTimeCal; //2mF档放电时间 10ms+修正时间(100uF增加1.6ms, 即8倍RC值), 根据充电时间来修正, 充电时间越长电容越大则放电时间也越长, 保证放点时间为6*2*C以上.
else if(level == 4) DischargeTime = 50 + DischargeTimeCal; // 20mF档放电时间 50ms+修正时间(100uF增加1.6ms, 即8倍RC值), 根据充电时间来修正, 充电时间越长电容越大则放电时间也越长, 保证放点时间为6*2*C以上.
index = 2; //下一步
}
else if(index == 2) //等待放电结束
{
if(DischargeTime != 0) //放电时间未到
{
if(--DischargeTime == 0) //放电结束
{
P_DISCHARGE = 0; //开始充电
NOP(12);
TR1 = 1; // Timer1开始计时
IE1 = 0; // 外中断1标志位
EX1 = 1; // 外部中断1允许
index = 3; //下一步
}
}
}
else if(index == 3) //等待充电结束
{
if(B_end) //充电结束
{
B_end = 0;
TR1 = 0; // 停止计数
EX1 = 0; // 外部中断1禁止
P_DISCHARGE = 1; // 放电
ChargeCnt++; //充电次数+1
if(UpdateTime != 0) index = 1; //更新时间未到, 则重复充放电一次
else //更新时间到
{
index = 0; //下一步重新开始
j = ((u32)CapCntH << 16) + ((u32)TH1 << 8) + (u32)TL1; //读计数值
j = j / 2 / ChargeCnt; //计算一个脉冲的时间(充电时间) , 分辨率1us(计数器始终为2MHz,周期为0.2us)
if(B_zero) // 归0
{
B_zero = 0;
C_zero = j; //电容归0
((u32 *)&tmp) = C_zero;
((u32 *)&tmp) = C_zero;
((u32 *)&tmp) = C_zero;
((u32 *)&tmp) = C_zero;
((u32 *)&tmp) = C_zero;
crc = MODBUS_CRC16(tmp,20);
tmp = (u8)(crc % 256);
tmp = (u8)(crc / 256);
EEPROM_SectorErase(0);
EEPROM_write_n(0, tmp, 22); // 保存EEPROM
}
j = j - C_zero; //0点修正
if(j & 0x80000000) j = -j, B_neg = 1, tmp = '-'; // 负数
else B_neg = 0, tmp = ' '; // 正数
j = (u32)((float)j * T_C_Sale); //校准系数
if(level == 2) DischargeTimeCal = (u16)(j / 600000UL); //200uF档 修正放电时间(100uF增加1.6ms, 即8倍RC值)
else if(level == 3) DischargeTimeCal = (u16)(j /60000UL); //2mF档 修正放电时间(100uF增加1.6ms, 即8倍RC值)
else if(level == 4) DischargeTimeCal = (u16)(j / 6000UL); // 20mF档 修正放电时间(100uF增加1.6ms, 即8倍RC值)
else DischargeTimeCal = 0;
if(level == 0) //2uF档, 1pF~2uF
{
if(j >= 1000000UL) // 0.001~2.000uF
{
DEC_x_xxx((u16)(j / 1000));
printf_ascii_6x8(114, 6, "uF");
}
else if(j >= 100000UL) // 0.1~200.0nF
{
DEC_xxx_x((u16)(j / 100));
printf_ascii_6x8(114, 6, "nF");
}
else if(j >= 10000UL) // 0.01~20.00nF
{
DEC_xx_xx((u16)(j / 10));
printf_ascii_6x8(114, 6, "nF");
}
else //0.001~2.000nF
{
DEC_x_xxx((u16)j);
printf_ascii_6x8(114, 6, "nF");
}
}
else if(level == 1) //20uF档, 0.01nF~20uF
{
if(j >= 1000000UL) // 0.01~20.00uF
{
DEC_xx_xx((u16)(j / 1000));
printf_ascii_6x8(114, 6, "uF");
}
else if(j >= 100000UL) // 0.001~2.000uF
{
DEC_x_xxx((u16)(j / 100));
printf_ascii_6x8(114, 6, "uF");
}
else if(j >= 10000UL) // 0.1~200.0nF
{
DEC_xxx_x((u16)(j / 10));
printf_ascii_6x8(114, 6, "nF");
}
else //0.01~20.00nF
{
DEC_xx_xx((u16)j);
printf_ascii_6x8(114, 6, "nF");
}
}
else if(level == 2) //200uF档, 0.1nF~200uF
{
if(j >= 1000000UL) // 0.1~200.0uF
{
DEC_xxx_x((u16)(j / 1000));
printf_ascii_6x8(114, 6, "uF");
}
else if(j >= 100000UL) // 0.01~20.00uF
{
DEC_xx_xx((u16)(j / 100));
printf_ascii_6x8(114, 6, "uF");
}
else if(j >= 10000UL) // 0.001~2.000uF
{
DEC_x_xxx((u16)(j / 10));
printf_ascii_6x8(114, 6, "uF");
}
else //0.1~200.0nF
{
DEC_xxx_x((u16)j);
printf_ascii_6x8(114, 6, "nF");
}
}
else if(level == 3) //2mF档, 0.001uF~2.000mF
{
if(j >= 1000000UL) // 0.001mF~2.000mF
{
DEC_x_xxx((u16)(j / 1000));
printf_ascii_6x8(114, 6, "mF");
}
else if(j >= 100000UL) // 0.1~200.0uF
{
DEC_xxx_x((u16)(j / 100));
printf_ascii_6x8(114, 6, "uF");
}
else if(j >= 10000UL) // 0.01~20.00uF
{
DEC_xx_xx((u16)(j / 10));
printf_ascii_6x8(114, 6, "uF");
}
else //0.001~2.000uF
{
DEC_x_xxx((u16)j);
printf_ascii_6x8(114, 6, "uF");
}
}
else if(level == 4) //20mF档, 10nF~20mF
{
if(j >= 1000000UL) // 0.01~20.00mF
{
DEC_xx_xx((u16)(j / 1000));
printf_ascii_6x8(114, 6, "mF");
}
else if(j >= 100000UL) // 0.001mF~2.000mF
{
DEC_x_xxx((u16)(j / 100));
printf_ascii_6x8(114, 6, "mF");
}
else if(j >= 10000UL) // 0.1~200.0uF
{
DEC_xxx_x((u16)(j / 10));
printf_ascii_6x8(114, 6, "uF");
}
else //0.01~20.00uF
{
DEC_xx_xx((u16)j);
printf_ascii_6x8(114, 6, "uF");
}
}
tmp = 0;
printf_ascii_10x24(40,5,tmp);
}
}
else if(LimitTime == 0) //超时了
{
index = 0; //下一步重新开始
ClearCapacitance(); //清除电容显示值
}
}
}
}
void ClearCapacitance(void) //清除电容显示值
{
printf_ascii_6x8(40, 5, " ");
printf_ascii_6x8(40, 6, " ");
printf_ascii_6x8(40, 7, " ");
}
void ShowLevel(void) // 显示档位
{
P_RELAY1 = 0; //RELAY1,推挽输出,高有效。 100R, 20mF档( 0.01uF~20.00mF)。
P_RELAY2 = 0; //RELAY2,推挽输出,高有效。 1K, 2mF档(0.001uF~2.000mF)。
P_RELAY3 = 0; //RELAY3,推挽输出,高有效。 10K, 200uF档(0.1nF~200.0uF)。
P_RELAY4 = 0; //RELAY4,推挽输出,高有效。 100K, 20uF档( 0.01nF~20.00uF)。RELAY1~4均断开则是2uF档(0.001nF~2.000uF).
DischargeTimeCal = 0;
index = 0;
if(level == 0) printf_ascii_6x8(0, 5, "2uF"), printf_ascii_6x8(0, 7, "R 1M"); // 1M, 2uF档。 RELAY1~4均断开则是2uF档(0.001nF~2.000uF).
else if(level == 1) printf_ascii_6x8(0, 5, " 20uF"), printf_ascii_6x8(0, 7, "R 100K"), P_RELAY4 = 1; // 100K, 2mF档。
else if(level == 2) printf_ascii_6x8(0, 5, "200uF"), printf_ascii_6x8(0, 7, "R 10K "), P_RELAY3 = 1; //10K,20mF档。
else if(level == 3) printf_ascii_6x8(0, 5, "2mF"), printf_ascii_6x8(0, 7, "R 1K"), P_RELAY2 = 1; // 1K, 2mF档。
else if(level == 4) printf_ascii_6x8(0, 5, " 20mF"), printf_ascii_6x8(0, 7, "R 100R"), P_RELAY1 = 1; // 100R,20mF档。
ClearCapacitance(); //清除电容显示值
}
//将数字显示成 x.xxx
void DEC_x_xxx(u16 j)
{
if(DotState != 1) //当前小数点不是第1位,则先清除显示
{
ClearCapacitance(); //清除电容显示值
DotState = 1;
}
tmp = j / 1000 + '0';
tmp = '.';
tmp = (j % 1000) / 100 + '0';
tmp = (j % 100) / 10 + '0';
tmp =j % 10 + '0';
}
//将数字显示成 xx.xx
void DEC_xx_xx(u16 j)
{
if(DotState != 2) //当前小数点不是第2位,则先清除显示
{
ClearCapacitance(); //清除电容显示值
DotState = 2;
}
tmp = j / 1000 + '0';
tmp = (j % 1000) / 100 + '0';
tmp = '.';
tmp = (j % 100) / 10 + '0';
tmp =j % 10 + '0';
if(tmp == '0') tmp = ' ';
}
//将数字显示成 xxx.x
void DEC_xxx_x(u16 j)
{
if(DotState != 3) //当前小数点不是第3位,则先清除显示
{
ClearCapacitance(); //清除电容显示值
DotState = 3;
}
tmp = j / 1000 + '0';
tmp = (j % 1000) / 100 + '0';
tmp = (j % 100) / 10 + '0';
tmp = '.';
tmp =j % 10 + '0';
if(tmp == '0')
{
tmp = ' ';
if(tmp == '0') tmp = ' ';
}
}
/************************ 比较器配置 ****************************/
void Compare_Config(void) //比较器初始化
{
u8 i;
CMPCR1 = 0;
CMPCR2 = 24; //比较结果变化延时周期数, 0~63
CMPCR1 |= (1<<7); //1: 允许比较器, 0:关闭比较器
CMPCR1 |= (0<<5); //1: 允许上升沿中断, 0: 禁止
CMPCR1 |= (0<<4); //1: 允许下降沿中断, 0: 禁止
CMPCR1 |= (0<<3); //输入正极性选择, 0: 选择外部P3.7做正输入, 1: 由ADC_CHS所选择的ADC输入端做正输入.
CMPCR1 |= (0<<2); //输入负极性选择, 0: 选择内部BandGap电压BGv做负输入, 1: 选择外部P3.6做输入
CMPCR1 |= (1<<1); //1: 允许比较结果输出到IO(P3.4或P4.1),0: 比较结果禁止输出到IO
CMPCR2 |= (0<<7); //1: 比较器结果输出IO取反, 0: 不取反
CMPCR2 |= (0<<6); //0: 允许内部0.1uF滤波, 1: 关闭
P_SW2 |= 0x80; //SFR enable
i = 0;
i |= (0<<6); //比较器迟滞输入选择: 0: 0mV,1: 10mV, 2: 20mV, 3: 30mV
i |= (0<<2); //输入负极性选择, 0: 选择P3.6做输入, 1: 选择内部BandGap电压BGv做负输入.
i |=0; //输入正极性选择, 0: 选择P3.7做输入, 1: 选择P5.0做输入,2: 选择P5.1做输入,3: 选择ADC输入(由ADC_CHS所选择的ADC输入端做正输入).
CMPEXCFG = i;
CMPO_P34(); //结果输出到P3.4.
// CMPO_P41(); //结果输出到P4.1.
P3n_push_pull(Pin4); //P3.4设置为推挽输出
P3n_pure_input(0xc0); //设置要做ADC的IO做高阻输入(P3.7 P3.6)
}
//========================================================================
// 函数: void Timer1_config(void)
// 描述: 初始化Timer1。
// 参数: none.
// 返回: none.
// 版本: VER1.0
// 日期: 2010-12-15
// 备注:
//========================================================================
void Timer1_config(void)
{
Timer1_AsTimer();
Timer1_16bit();
Timer1_12T();
Timer1_Gate_INT1_P33(); //INT1高电平计时
TH1= 0;
TL1= 0;
CapCntH = 0;
ET1 = 1; //允许T1中断
IT1 = 1; // 外部中断1下降沿中断
}
/********************* INT1中断函数 *************************/
void INT1_ISR (void) interrupt 2
{
TR1 = 0;
P_DISCHARGE = 1; //放电
B_end = 1;
}
//========================================================================
// 函数: void timer1 (void) interrupt 3
// 描述: Timer1中断函数,模拟PWM的核心函数。
// 参数: none.
// 返回: none.
// 版本: VER1.0
// 日期: 2010-12-15
// 备注:
//========================================================================
void timer1_ISR (void) interrupt 3
{
CapCntH++;
}
//========================================================================
// 函数: u16 MODBUS_CRC16(u8 *p, u8 n)
// 描述: 计算CRC16函数.
// 参数: *p: 要计算的数据指针.
// n: 要计算的字节数.
// 返回: CRC16值.
// 版本: V1.0, 2022-3-18 梁工
//========================================================================
u16 MODBUS_CRC16(u8 *p, u8 n)
{
u8 i;
u16 crc16;
crc16 = 0xffff; //预置16位CRC寄存器为0xffff(即全为1)
do
{
crc16 ^= (u16)*p; //把8位数据与16位CRC寄存器的低位相异或,把结果放于CRC寄存器
for(i=0; i<8; i++) //8位数据
{
if(crc16 & 1) crc16 = (crc16 >> 1) ^ 0xA001; //如果最低位为0,把CRC寄存器的内容右移一位(朝低位),用0填补最高位,
//再异或多项式0xA001
else crc16 >>= 1; //如果最低位为0,把CRC寄存器的内容右移一位(朝低位),用0填补最高位
}
p++;
}while(--n != 0);
return (crc16);
}
//OLED128x64-SSD1306-I2C-V30.c 中的程序
/* ---技术交流: www.STCAIMCU.com ------------------------------------*/
/* ---资料下载: www.STCAI.com -----------------------------------------*/
/* --- Tel: 86-0513-55012928,55012929,55012966 ---------------------*/
/* If you want to use the program or the program referenced in your article,
please specify in which data and procedures from STC */
#include "config.h"
#include "ASCII5x7.h"
#include "ASCII-10x24.h"
/************* 功能说明 **************
淘宝买的OLED屏驱动程序. 驱动IC为SSD1306, I2C接口.
支持中文英文混显, 支持10x24点大数字显示.
******************************************/
//************************************************************************************************
/* 定义接口 */
// 1--GND D2 SDAout D/C --> SA0 从机地址最低位, 模块接到0了
// 2--VCC 3.3V E ----> low RES --> 复位脚, 低电平复位
sbit SCL = P1^0; // 3--D0 SCL R/W --> low
sbit SDA = P1^1; // 4--D1 SDAin CS ---> low
#define SLAW 0x78 /* 从机地址写 */
#define SLAR 0x79 /* 从机地址读 */
//========================================================================
// 描述: 延时函数
// 参数: dly 延时时间ms, 1~65535.
// 返回: 无
//========================================================================
void OLED_delay_ms(u16 ms) // 1~65535
{
u16 i;
do
{
i = MAIN_Fosc / 10000; //STC8系列
while(--i) ;
}while(--ms);
}
/****************************/
void OLED_I2C_Delay(void)
{
u8 dly;
dly = (MAIN_Fosc * 2) / 3000000UL; //周期数: 6+3*n, I2C位时间调整, (Fosc/1000000) * n / 3 = (MAIN_Fosc * n) / 3000000UL, n为us, 3为while(--dly)的周期数
while(--dly) ;
}
#define OLED_I2C_NOP() NOP(3) // 至少加3个NOP @24MHz, 至少加4个NOP @36MHz, 至少加5个NOP @44MHz, 否则显示不正常 STC8H8K64U
/****************************/
void OLED_I2C_Start(void) //start the I2C, SDA High-to-low when SCL is high
{
SDA = 1;
// OLED_I2C_Delay();
SCL = 1;
OLED_I2C_Delay();
SDA = 0;
OLED_I2C_Delay();
SCL = 0;
// OLED_I2C_Delay();
}
void OLED_I2C_Stop(void) //STOP the I2C, SDA Low-to-high when SCL is high
{
SDA = 0;
OLED_I2C_Delay();
SCL = 1;
OLED_I2C_Delay();
SDA = 1;
OLED_I2C_Delay();
}
/********** 写一个字节并读出应答放F0(正确应答F0=0, 错误F0=1) ******************/
void OLED_I2C_WriteAbyte(u8 dat) //write a byte to I2C 速度: 24MHZ, 12T/bit, 107+8=115T, 4.8us/byte.
{
B = dat;
SDA = B7; OLED_I2C_NOP(); SCL = 1; OLED_I2C_NOP(); SCL = 0; OLED_I2C_NOP();
SDA = B6; OLED_I2C_NOP(); SCL = 1; OLED_I2C_NOP(); SCL = 0; OLED_I2C_NOP();
SDA = B5; OLED_I2C_NOP(); SCL = 1; OLED_I2C_NOP(); SCL = 0; OLED_I2C_NOP();
SDA = B4; OLED_I2C_NOP(); SCL = 1; OLED_I2C_NOP(); SCL = 0; OLED_I2C_NOP();
SDA = B3; OLED_I2C_NOP(); SCL = 1; OLED_I2C_NOP(); SCL = 0; OLED_I2C_NOP();
SDA = B2; OLED_I2C_NOP(); SCL = 1; OLED_I2C_NOP(); SCL = 0; OLED_I2C_NOP();
SDA = B1; OLED_I2C_NOP(); SCL = 1; OLED_I2C_NOP(); SCL = 0; OLED_I2C_NOP();
SDA = B0; OLED_I2C_NOP(); SCL = 1; OLED_I2C_NOP(); SCL = 0; OLED_I2C_NOP();
/*
SDA = B7; OLED_I2C_NOP(); SCL = 1; NOP(2); SCL = 0; OLED_I2C_NOP();
SDA = B6; OLED_I2C_NOP(); SCL = 1; NOP(2); SCL = 0; OLED_I2C_NOP();
SDA = B5; OLED_I2C_NOP(); SCL = 1; NOP(2); SCL = 0; OLED_I2C_NOP();
SDA = B4; OLED_I2C_NOP(); SCL = 1; NOP(2); SCL = 0; OLED_I2C_NOP();
SDA = B3; OLED_I2C_NOP(); SCL = 1; NOP(2); SCL = 0; OLED_I2C_NOP();
SDA = B2; OLED_I2C_NOP(); SCL = 1; NOP(2); SCL = 0; OLED_I2C_NOP();
SDA = B1; OLED_I2C_NOP(); SCL = 1; NOP(2); SCL = 0; OLED_I2C_NOP();
SDA = B0; OLED_I2C_NOP(); SCL = 1; NOP(2); SCL = 0; OLED_I2C_NOP();
*/
SDA = 1;
OLED_I2C_NOP();
SCL = 1;
OLED_I2C_NOP();
F0 = SDA;
SCL = 0;
// OLED_I2C_NOP();
}
void OLED_WriteCMD(u8 cmd) // 写命令cmd
{
OLED_I2C_Start();
OLED_I2C_WriteAbyte(SLAW); //Slave address,SA0=0
if(!F0)
{
OLED_I2C_WriteAbyte(0x00); //Write command, C0=0, D/C=0, follow by six '0'
if(!F0)
{
OLED_I2C_WriteAbyte(cmd); //command
}
}
OLED_I2C_Stop();
}
//========================================================================
// 函数: void Set_Dot_Addr_LCD(int x,int y)
// 描述: 设置在LCD的真实坐标系上的X、Y点对应的RAM地址
// 参数: x X轴坐标
// y Y轴坐标
// 返回: 无
// 备注: 仅设置当前操作地址,为后面的连续操作作好准备
// 版本:
// 2007/04/10 First version
//========================================================================
void Set_Dot_Addr(u8 x, u8 y) //x为横向的点0~127, y为纵向的页0~7
{
OLED_I2C_Start();
OLED_I2C_WriteAbyte(SLAW); //Slave address,SA0=0
OLED_I2C_WriteAbyte(0x00); //Write command, C0=0, D/C=0, follow by six '0'
OLED_I2C_WriteAbyte(0xb0 + y); //设置页0~7
OLED_I2C_WriteAbyte(0x00); //Write command, C0=0, D/C=0, follow by six '0'
OLED_I2C_WriteAbyte((x >> 4)| 0x10); //设置列0~127 高nibble
OLED_I2C_WriteAbyte(0x00); //Write command, C0=0, D/C=0, follow by six '0'
OLED_I2C_WriteAbyte(x & 0x0f); //设置列0~127 低nibble
OLED_I2C_Stop();
}
//******************************************
void FillPage(u8 y, u8 color) //Fill Page LCD RAM, y为页码 0~7
{
u8 i;
Set_Dot_Addr(0,y);
OLED_I2C_Start();
OLED_I2C_WriteAbyte(SLAW); //Slave address,SA0=0
if(!F0)
{
OLED_I2C_WriteAbyte(0x40); //write data,C0=0, D/C=1, follow by six '0'
if(!F0)
{
for(i=0; i<128; i++) OLED_I2C_WriteAbyte(color);
}
}
OLED_I2C_Stop();
}
//******************************************
void FillAll(u8 color) //Fill all LCD RAM, color为填充的颜色
{
u8 i;
for(i=0; i<8; i++) FillPage(i,color);
}
//******************************************
void OLED_config(void) //初始化函数
{
// P1.0--OLED12864-SCL 开漏
// P1.1--OLED12864-SDA 开漏
P1n_open_drain(Pin1+Pin0);
SCL = 1;
SDA = 1;
OLED_delay_ms(10);
OLED_WriteCMD(0xAE); //Set Display Off
OLED_WriteCMD(0xd5); //display divide ratio/osc. freq. mode
OLED_WriteCMD(0x80); //
OLED_WriteCMD(0xA8); //multiplex ration mode:63
OLED_WriteCMD(0x3F);
OLED_WriteCMD(0xD3); //Set Display Offset
OLED_WriteCMD(0x00);
OLED_WriteCMD(0x40); //Set Display Start Line
OLED_WriteCMD(0x8D); //Set Display Offset
OLED_WriteCMD(0x14);
OLED_WriteCMD(0xA1); //Segment Remap
OLED_WriteCMD(0xC8); //Sst COM Output Scan Direction
OLED_WriteCMD(0xDA); //common pads hardware: alternative
OLED_WriteCMD(0x12);
OLED_WriteCMD(0x81); //contrast control
OLED_WriteCMD(0xCF);
OLED_WriteCMD(0xD9); //set pre-charge period
OLED_WriteCMD(0xF1);
OLED_WriteCMD(0xDB); //VCOM deselect level mode
OLED_WriteCMD(0x40); //set Vvcomh=0.83*Vcc
OLED_WriteCMD(0xA4); //Set Entire Display On/Off
OLED_WriteCMD(0xA6); //Set Normal Display
FillAll(0);
OLED_WriteCMD(0xAF); //Set Display On
}
//******************************************
/*
void WriteAscii_6x8(u8 x, u8 y, u8 chr) //向指定位置写一个ASCII码字符, x为横向的点0~127, y为纵向的页0~7, chr为要写的字符
{
u8 code *p;
u8 i;
if(x > (128-5)) return;
if(y >= 8) return;
p = (u16)chr * 6 + ASCII5x7;
Set_Dot_Addr(x,y);
OLED_I2C_Start();
OLED_I2C_WriteAbyte(SLAW); //Slave address,SA0=0
if(!F0)
{
OLED_I2C_WriteAbyte(0x40); //write data,C0=0, D/C=1, follow by six '0'
if(!F0)
{
for(i=0; i<6; i++)
{
OLED_I2C_WriteAbyte(*p);
p++;
}
}
}
OLED_I2C_Stop();
}
*/
/************ 打印ASCII字符串 *************************/
void printf_ascii_6x8(u8 x, u8 y, u8 *ptr) //x为横向的点0~127, y为纵向的页0~7, *ptr为要打印的字符串指针.打印21个ASCII码(21*6+9=135个字节)耗时625us@44MHZ, 652us@36MHz, 850us@24MHz
{
u8 c;
u8 i;
u8 code *p;
if(y >= 8) return;
Set_Dot_Addr(x,y);
OLED_I2C_Start();
OLED_I2C_WriteAbyte(SLAW); //Slave address,SA0=0
if(!F0)
{
OLED_I2C_WriteAbyte(0x40); //write data,C0=0, D/C=1, follow by six '0'
if(!F0)
{
for (;;)
{
c = *ptr;
if(c == 0) break; //遇到停止符0结束
if(c >= 0xa0) break; //非ASCII码结束
p = (u16)c * 6 + ASCII5x7;
for(i=0; i<6; i++)
{
OLED_I2C_WriteAbyte(*p);
p++;
}
x += 6;
ptr++;
if(x > (128-5)) break;
}
}
}
OLED_I2C_Stop();
}
//******************************************
/*
void WriteAscii_8x16(u8 x, u8 y, u8 chr) //向指定位置写一个ASCII码字符, x为横向的点0~127, y为纵向的页0~7, chr为要写的字符
{
u8 code *p;
u8 i;
if(x > (128-8)) return;
if(y > 6) return;
if(chr == 'A') chr = 0;
else if(chr == 'V') chr = 1;
else if(chr == 'W') chr = 2;
else return;
p = (u16)chr * 16 + ASCII8x16;
Set_Dot_Addr(x,y);
OLED_I2C_Start();
OLED_I2C_WriteAbyte(SLAW); //Slave address,SA0=0
if(!F0)
{
OLED_I2C_WriteAbyte(0x40); //write data,C0=0, D/C=1, follow by six '0'
if(!F0)
{
for(i=0; i<8; i++)
{
OLED_I2C_WriteAbyte(*p);
p++;
}
}
}
OLED_I2C_Stop();
Set_Dot_Addr(x,y+1);
OLED_I2C_Start();
OLED_I2C_WriteAbyte(SLAW); //Slave address,SA0=0
if(!F0)
{
OLED_I2C_WriteAbyte(0x40); //write data,C0=0, D/C=1, follow by six '0'
if(!F0)
{
for(i=0; i<8; i++)
{
OLED_I2C_WriteAbyte(*p);
p++;
}
}
}
OLED_I2C_Stop();
}
*/
//******************************************
void WriteAscii_10x24(u8 x, u8 y, u8 chr) //向指定位置写一个ASCII码字符, x为横向的点0~127, y为纵向的页0~7, chr为要写的字符
{
u8 code *p;
u8 i;
if(x > (128-10)) return;
if(y >= 6) return;
p = (u16)chr * 30 + ASCII10x24;
Set_Dot_Addr(x,y);
OLED_I2C_Start();
OLED_I2C_WriteAbyte(SLAW); //Slave address,SA0=0
if(!F0)
{
OLED_I2C_WriteAbyte(0x40); //write data,C0=0, D/C=1, follow by six '0'
if(!F0)
{
for(i=0; i<10; i++)
{
OLED_I2C_WriteAbyte(*p);
p++;
}
}
}
OLED_I2C_Stop();
Set_Dot_Addr(x,y+1);
OLED_I2C_Start();
OLED_I2C_WriteAbyte(SLAW); //Slave address,SA0=0
if(!F0)
{
OLED_I2C_WriteAbyte(0x40); //write data,C0=0, D/C=1, follow by six '0'
if(!F0)
{
for(i=0; i<10; i++)
{
OLED_I2C_WriteAbyte(*p);
p++;
}
}
}
OLED_I2C_Stop();
Set_Dot_Addr(x,y+2);
OLED_I2C_Start();
OLED_I2C_WriteAbyte(SLAW); //Slave address,SA0=0
if(!F0)
{
OLED_I2C_WriteAbyte(0x40); //write data,C0=0, D/C=1, follow by six '0'
if(!F0)
{
for(i=0; i<10; i++)
{
OLED_I2C_WriteAbyte(*p);
p++;
}
}
}
OLED_I2C_Stop();
}
//******************************************
void WriteDot_3x3(u8 x, u8 y) //向指定位置写一个小数点,x为横向的点0~127, y为纵向的页0~7
{
if(x > (128-3)) return;
if(y >= 8) return;
Set_Dot_Addr(x,y);
OLED_I2C_Start();
OLED_I2C_WriteAbyte(SLAW); //Slave address,SA0=0
if(!F0)
{
OLED_I2C_WriteAbyte(0x40); //write data,C0=0, D/C=1, follow by six '0'
if(!F0)
{
OLED_I2C_WriteAbyte(0x38);
OLED_I2C_WriteAbyte(0x38);
OLED_I2C_WriteAbyte(0x38);
}
}
OLED_I2C_Stop();
}
//************ 打印ASCII 10x24英文字符串 *************************
void printf_ascii_10x24(u8 x, u8 y, u8 *ptr) //x为横向的点0~127, y为纵向的页0~7, *ptr为要打印的字符串指针, 间隔2点.
{
u8 c;
for (;;)
{
c = *ptr;
if(c == 0) return; //遇到停止符0结束
if((c >= '0') && (c <= '9')) //ASCII码
{
WriteAscii_10x24(x,y,c-'0');
x += 12;
ptr++;
}
else if(c == '.')
{
WriteDot_3x3(x,y+2);
x += 6;
ptr++;
}
else if(c == ' ') //显示空格
{
WriteAscii_10x24(x,y,11);
x += 12;
ptr++;
}
else if(c == '-') //显示空格
{
WriteAscii_10x24(x,y,10);
x += 12;
ptr++;
}
}
}
//EEPROM.c 中的程序
/* ---技术交流: www.STCAIMCU.com ------------------------------------*/
/* ---资料下载: www.STCAI.com -----------------------------------------*/
/* --- Tel: 86-0513-55012928,55012929,55012966 ---------------------*/
/* If you want to use the program or the program referenced in the article, */
please specify in which data and procedures from STC */
// 本程序是STC系列的内置EEPROM读写程序。
#include "config.h"
#include "eeprom.h"
//========================================================================
// 函数: void ISP_Disable(void)
// 描述: 禁止访问ISP/IAP.
// 参数: non.
// 返回: non.
// 版本: V1.0, 2012-10-22
//========================================================================
void DisableEEPROM(void)
{
ISP_CONTR = 0; //禁止ISP/IAP操作
IAP_TPS = 0;
ISP_CMD = 0; //去除ISP/IAP命令
ISP_TRIG= 0; //防止ISP/IAP命令误触发
ISP_ADDRH = 0xff; //清0地址高字节
ISP_ADDRL = 0xff; //清0地址低字节,指向非EEPROM区,防止误操作
}
//========================================================================
// 函数: void EEPROM_read_n(u16 EE_address,u8 *DataAddress,u16 number)
// 描述: 从指定EEPROM首地址读出n个字节放指定的缓冲.
// 参数: EE_address:读出EEPROM的首地址.
// DataAddress: 读出数据放缓冲的首地址.
// number: 读出的字节长度.
// 返回: non.
// 版本: V1.0, 2012-10-22
//========================================================================
void EEPROM_read_n(u16 EE_address,u8 *DataAddress,u16 number)
{
EA = 0; //禁止中断
ISP_CONTR = ISP_EN; //允许ISP/IAP操作
IAP_TPS = (u8)((MAIN_Fosc+500000L) / 1000000L); //工作频率设置
ISP_READ(); //送字节读命令,命令不需改变时,不需重新送命令
do
{
ISP_ADDRH = EE_address / 256; //送地址高字节(地址需要改变时才需重新送地址)
ISP_ADDRL = EE_address % 256; //送地址低字节
ISP_TRIG(); //先送5AH,再送A5H到ISP/IAP触发寄存器,每次都需要如此
//送完A5H后,ISP/IAP命令立即被触发启动
//CPU等待IAP完成后,才会继续执行程序。
_nop_();
_nop_();
_nop_();
*DataAddress = ISP_DATA; //读出的数据送往
EE_address++;
DataAddress++;
}while(--number);
DisableEEPROM();
EA = 1; //重新允许中断
}
/******************** 扇区擦除函数 *****************/
//========================================================================
// 函数: void EEPROM_SectorErase(u16 EE_address)
// 描述: 把指定地址的EEPROM扇区擦除.
// 参数: EE_address:要擦除的扇区EEPROM的地址.
// 返回: non.
// 版本: V1.0, 2013-5-10
//========================================================================
void EEPROM_SectorErase(u16 EE_address)
{
EA = 0; //禁止中断
//只有扇区擦除,没有字节擦除,512字节/扇区。
//扇区中任意一个字节地址都是扇区地址。
ISP_ADDRH = EE_address / 256; //送扇区地址高字节(地址需要改变时才需重新送地址)
ISP_ADDRL = EE_address % 256; //送扇区地址低字节
ISP_CONTR = ISP_EN; //允许ISP/IAP操作
IAP_TPS = (u8)(MAIN_Fosc / 1000000L); //工作频率设置
ISP_ERASE(); //送扇区擦除命令,命令不需改变时,不需重新送命令
ISP_TRIG();
_nop_();
_nop_();
_nop_();
DisableEEPROM();
EA = 1; //重新允许中断
}
//========================================================================
// 函数: void EEPROM_write_n(u16 EE_address,u8 *DataAddress,u16 number)
// 描述: 把缓冲的n个字节写入指定首地址的EEPROM.
// 参数: EE_address:写入EEPROM的首地址.
// DataAddress: 写入源数据的缓冲的首地址.
// number: 写入的字节长度.
// 返回: non.
// 版本: V1.0, 2012-10-22
//========================================================================
void EEPROM_write_n(u16 EE_address,u8 *DataAddress,u16 number)
{
EA = 0; //禁止中断
ISP_CONTR = ISP_EN; //允许ISP/IAP操作
IAP_TPS = (u8)(MAIN_Fosc / 1000000L); //工作频率设置
ISP_WRITE(); //送字节写命令,命令不需改变时,不需重新送命令
do
{
ISP_ADDRH = EE_address / 256; //送地址高字节(地址需要改变时才需重新送地址)
ISP_ADDRL = EE_address % 256; //送地址低字节
ISP_DATA= *DataAddress; //送数据到ISP_DATA,只有数据改变时才需重新送
ISP_TRIG();
_nop_();
_nop_();
_nop_();
EE_address++;
DataAddress++;
}while(--number);
DisableEEPROM();
EA = 1; //重新允许中断
}
赞, 制作提示:
1、档位放电电阻切换使用磁簧继电器,不用MOSFET,D、S极电容大,影响测量,放电管用MOSFET实在是没有办法的事。磁簧继电器隔离性能好,寿命长(近1亿次),无噪音,体积小,驱动电流小(IO直接驱动),触点电阻几十毫欧,触点电容可忽略。
2、档位电阻使用5色环金属膜电阻,1%误差。
3、使用外部24M晶振,读数稳定。
4、电容表供电使用5V,78M05稳压的,输入至少7.2V,一般7.2~15V。磁簧继电器需要5V驱动,5V也能使模拟比较器工作的动态电压更大(充电终止电压3.16V)。
5、关于精度,理论上只要给定参考电压 U*(1-1/e)=U*0.63212 精确,即R4、R5的分压比精确,则测量就会很准确,不需要另外标定。当然,有标准电容标定一下更好。
测量1nF以下的小电容,特别是几十PF的小电容误差可能较大,达到5%,因为用1M电阻充放电,放电管的D、S电容跟电压敏感,还有电路的分布电容影响,往往不是读数归0就能消除的。
有些种类的电容公分电压有关系,电压不同电容量不同,可以认为增加偏压再测量。
本电容表测量时先将电容放电至0V,再充电到3.16V,所以测出的电容量就是电容在1~3V电压时的电容量,只要工作中电容的电压差不多,则测量的电容就是其工作时的容量,正是我所需要的。
如果用数字电桥测量对比,会发现有的电容容量偏差比较大,那是因为数字电桥的测量方法不同,激励信号比较小(一般是交流200mV或200mV),比如测量MOSFET的DS电容会比我的表大好多,因为DS电容在0电压比在几V时大了几倍。同一个电容、同一个数字电桥在不同的测试频率和电压,电容都会不同,甚至相差比较大,因为电容还是频率的函数。
测量电容有好多方法:
1、数字电桥,可以测量好多参数,这个比较好,但是电路复杂,程序更复杂,正在设计电路。
2、容抗法,大部分数字万用表附加的测电容功能都是,但是只能测到20uF更大的就是200uF,因为大电容的容抗太小了不好处理,万用表用的400Hz正弦波测试的,测小电容可以用。
3、谐振法,网上有N多电路例子,就是用外接模拟比较器+LC振荡测频率再计算L或C,这个还可以的,但是对制作要求高点,参考电感和参考电容要有好的性能。谐振的热蠕动比较讨厌。
4、充放电(或方波振荡)法,就是本表用的,简单、稳定,可以测量从pF到几十mF,对于大部分工程的使用都合适了。
5、线性三角波激励测量,这个我实际打板测试过,可以,但是电路稍复杂,需要标定,可以测量10PF~40mF,还可以测电感(1uH~200mH,大电感不好做)。 【USB 原理及实战,16课时】,视频教学已完美完成 ,大学标准课程 !
【10月/9号,10月/11号】USB基本原理教学视频, 已上传
【10月/16号的 USB-HID 通信 实战】教学视频超级完美, 已上传
是对着协议和代码一行一行的讲解,认真听的都说会了
【10月/18号下午的 USB-CDC虚拟串口 实战】教学视频, 已上传
USB-CDC虚拟串口 / 就是最简单最强大的串口
是对着协议和代码一行一行的讲解,认真听的都说会了
请帮忙转发给可能需要:从0开始了解 USB 的 同学/同事/老师/研发人员
https://www.stcaimcu.com/forum.php?mod=viewthread&tid=4526&extra=&page=1
=========================================
【CAN 原理及实战,8课时】,教学视频,制作中,后续直接看视频回放
期待梁工的“数字电桥”{:lol:} rz12345 发表于 2023-11-6 08:48
期待梁工的“数字电桥”
电路设计好了,正在画PCB。