Skip to content

51 外设扩展 — I2C 与 SPI

一、I2C — PCF8591(8 位 ADC + DAC)

芯片特性

  • ADC/DAC 分辨率:8 位(ADC 4 通道)
  • 通信接口:I2C,最高 100kHz
  • 单次转换时间:约 90μs
  • 工作电压:2.5V ~ 6V

引脚定义

  • AIN0~AIN3:4 路模拟信号输入
  • A0~A2:I2C 硬件地址脚
  • SDA、SCL:I2C 数据线、时钟线
  • AOUT:D/A 模拟输出
  • VREF:基准电压
  • OSC:外部时钟输入

ADC 转换公式

当 VREF = 5V 时,最小分辨电压为 19.53mV


I2C 寻址字节

假设 A2、A1、A0 都接地:

操作地址
写操作0x90
读操作0x91
c
#define AddrWR  0x90
#define AddrRD  0x91

控制寄存器

功能
D7固定为 0
D6模拟量输出允许(1=允许 DAC)
D5、D4模拟输入方式:00=四路单端,01=三路差分,10=单端+差分,11=两路差分
D3固定为 0
D2自动加 1 选择通道(1=使能)
D1、D0通道选择:00=CH0,01=CH1,10=CH2,11=CH3

练习:选择通道 2,禁用自动加 1,单端输入,禁用模拟输出

D6=0(禁用 DAC)、D5/D4=00(单端)、D2=0(不自动加 1)、D1/D0=10(通道 2)


A/D 转换驱动函数

c
uchar PCF8591_ADC(uchar ch)
{
    uchar dat;
    Start();                    // I2C 起始信号
    SendByte(0x90);            // 写寻址字节
    Ack();                     // 读从机应答
    SendByte(0x40 | ch);       // 写控制字,选择 A/D 通道
    Ack();                     // 读 A/D 应答
    Start();                    // 重复起始
    SendByte(0x91);            // 读 A/D 转换结果
    Ack();                     // 读从机应答
    dat = RcvByte();           // 开始读转换结果
    NoAck();                   // 主机发 NACK,只读 1 次
    Stop();                    // I2C 停止信号
    return dat;
}

注意事项

  1. 写控制字前需要先写从机地址
  2. 读到的是上一次转换的结果(需先启一次转换,再读)
  3. 这就是为什么代码中要先写控制字,再发一次起始+读地址

D/A 转换驱动函数

c
void PCF8591_DAC(uchar dat)
{
    Start();
    SendByte(0x90);   // 写寻址字节
    Ack();
    SendByte(0x40);   // 写控制字(D6=1,允许 DAC)
    Ack();
    SendByte(dat);    // 写待转换的数字量
    NoAck();
    Stop();
}

二、SPI — TLC549(8 位 ADC)

引脚定义

引脚功能
REF+、REF-基准电压(2.5V ≤ REF+ ≤ VCC+0.1V)
CS片选,低电平有效
DATA OUT数据串行输出
ANALOG IN模拟信号输入
I/O CLOCK时钟输入(≤ 1.1MHz)

转换公式

当 REF+=5V,REF- = 0V 时:

工作时序要点

  • CS 拉低 → 启动一次读,同时启动新转换
  • CLOCK 下降沿 读每一位
  • CS 拉高 → 结束本次传输,等待 ≥ 17μs

接口函数

c
#define uchar unsigned char

sbit TLC549_CLK = P2^3;
sbit TLC549_DO  = P2^4;
sbit TLC549_CS  = P2^5;

uchar TLC549_ADC(void)
{
    uchar i, temp;
    TLC549_CLK = 0;
    TLC549_CS  = 0;        // 片选,启动转换

    for(i = 0; i < 8; i++)
    {
        temp <<= 1;
        temp |= TLC549_DO;  // 读当前位
        TLC549_CLK = 1;     // 采样下一位
        TLC549_CLK = 0;     // 准备读
    }

    TLC549_CS = 1;          // 取消选中
    delayus(20);            // 等待转换完成
    return temp;            // 返回 8 位数据
}

三、SPI — TLC5615(10 位 DAC)

引脚定义

引脚功能
DIN串行数据输入
SCLK串行时钟(≤ 14MHz)
CS片选,低有效
DOUT级联数据输出
REFIN基准电压(2V~VDD-2V)
OUTDAC 模拟电压输出

转换公式

帧格式

两种可选:12 位或 16 位

  • 12 位格式:数据左移 6 位(dat <<= 6),传 12 个时钟
  • 16 位格式:数据左移 2 位(dat <<= 2),传 16 个时钟

12 位接口函数

c
void TLC5615_DAC(uint dat)
{
    uchar i;
    dat <<= 6;                   // 左移 6 位,补 6 个 0

    TLC5615_CLK = 0;
    TLC5615_CS  = 0;

    for(i = 0; i < 12; i++)
    {
        TLC5615_DI = (bit)(dat & 0x8000);  // 取 MSB
        TLC5615_CLK = 0;
        dat <<= 1;
        TLC5615_CLK = 1;                    // 输入当前位
    }

    TLC5615_CS  = 1;
    TLC5615_CLK = 0;
    delayus(20);                            // 建立时间 ≥ 12.5μs
}

16 位接口函数

c
void TLC5615_DAC(uint dat)
{
    uchar i;
    dat <<= 2;                   // 左移 2 位,补 2 个 0

    TLC5615_CLK = 0;
    TLC5615_CS  = 0;

    for(i = 0; i < 16; i++)
    {
        TLC5615_DI = (bit)(dat & 0x8000);
        TLC5615_CLK = 0;
        dat <<= 1;
        TLC5615_CLK = 1;
    }

    TLC5615_CS  = 1;
    TLC5615_CLK = 0;
    delayus(20);
}

应用:正弦波发生器(TLC5615)

每隔 1ms 输出一个正弦波数据,256 个点频率约 4Hz:

c
#include <reg52.h>
#define uchar unsigned char
#define uint unsigned int

uchar code sin[256] = {/* 正弦波表,预计算 */};

uint count = 0;             // 正弦波表下标

sbit TLC5615_DI  = P2^0;
sbit TLC5615_CLK = P2^1;
sbit TLC5615_CS  = P2^2;

void main(void)
{
    // T0 方式 1,1ms 定时
    TMOD |= 0x01;
    TH0 = (65536 - 1000) / 256;
    TL0 = (65536 - 1000) % 256;

    EA  = 1;        // 开总中断
    ET0 = 1;        // 开 T0 中断
    TR0 = 1;        // 启动 T0

    while(1);       // 空循环,等待中断
}

void T0_ISR() interrupt 1
{
    TH0 = (65536 - 1000) / 256;    // 重装初值
    TL0 = (65536 - 1000) % 256;

    TLC5615_DAC(sin[count]);       // 输出正弦波
    if(++count == 256)
        count = 0;
}

TLC549 + LCD1602 电压表

采集电压并显示在 LCD1602 第 1 行中间:

c
TMOD |= 0x01;
TH0 = (65536 - 1000)/256;  // 1ms
TL0 = (65536 - 1000)%256;
// ... 初始化 ...

void main(void)
{
    uint temp;
    lcd_ini();                    // LCD 初始化
    while(1)
    {
        temp = TLC549_ADC();

        w_com(0x84);                                   // 第 1 行第 5 列
        w_dat(temp * 195 / 10000 + 0x30);             // 电压个位数
        w_dat('.');                                     // 小数点
        w_dat(temp * 195 % 10000 / 1000 + 0x30);      // 第 1 位小数
        w_dat(temp * 195 % 1000 / 100 + 0x30);        // 第 2 位小数
        w_dat('V');
    }
}

temp * 195 / 10000 实际是 (因用整数运算简化为乘 195 除 10000)。

Copyright © 2026