第二十八章 51单片机配置 SPI 及其简单应用

1. 导入

SPI(Serial Peripheral Interface)是主从式全双工同步串行总线,常见于 Flash、显示屏、DAC/ADC、IO 扩展等外设。标准 8051 多无硬件 SPI 外设,但可用软件“位操作(bit-bang)”轻松实现;增强型 51(如 STC8 系列)自带 SPI,效率更高。

本章目标:

  • 搭建通用软件 SPI(支持 Mode0/Mode3);
  • 演示两个典型应用:W25Qxx SPI Flash 读 JEDEC ID、MAX7219 数码管/点阵驱动显示;
  • 简述硬件 SPI(以 STC8 为例)的基本配置思路。

2. 硬件设计

  • SPI 四线:SCK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选,低有效)。部分器件还会有 RST、INT、BUSY 等控制脚。
  • 电平与供电:
    • 很多 SPI 外设是 3.3V 逻辑(如 W25Qxx、彩屏模块)。若 51 为 5V 供电,请确认模块是否带电平转换;否则需电平转换或全 3.3V 运行。
  • 推荐接线(可按需修改端口):
    • P1.3 → SCKP1.4 → MOSIP1.5 ← MISO
    • P1.0 → CS_FLASH(W25Qxx),P1.1 → CS_MAX(MAX7219)
  • 去耦:每个芯片 VCC-GND 近端放置 0.1µF;Flash 等对电源较敏感,可再并 10µF。

3. SPI 基础与模式

  • 时序参数由 CPOLCPHA 决定,常用两种:
    • Mode 0:CPOL=0, CPHA=0(空闲 SCK 低,数据在上升沿采样)
    • Mode 3:CPOL=1, CPHA=1(空闲 SCK 高,数据在下降沿采样)
  • 位序:大多数器件 MSB 先行(高位先出)。

本章软件 SPI 默认 Mode 0,如需 Mode 3 可一处宏切换。


4. 通用软件 SPI 驱动(Mode0/Mode3 可切换)

#include <reg52.h>
#include <intrins.h>

/* -------- SPI 引脚映射(按需修改) -------- */
sbit SPI_SCK  = P1^3;
sbit SPI_MOSI = P1^4;
sbit SPI_MISO = P1^5;

/* 可为不同从设备分配各自 CS 管脚 */
sbit CS_FLASH = P1^0;   // W25Qxx
sbit CS_MAX   = P1^1;   // MAX7219

/* -------- SPI 模式选择 --------
   0 = Mode 0: CPOL=0, CPHA=0(空闲低,上升沿采样)
   3 = Mode 3: CPOL=1, CPHA=1(空闲高,下降沿采样) */
#define SPI_MODE 0

/* -------- 速度控制:增减 _nop_ 次数可调速 -------- */
static void spi_delay(void){
    _nop_(); _nop_(); _nop_(); _nop_();
}

/* -------- SPI 引脚初始化 -------- */
void spi_gpio_init(void){
#if (SPI_MODE==0)
    SPI_SCK = 0;  // 空闲低
#else
    SPI_SCK = 1;  // 空闲高
#endif
    SPI_MOSI = 0;
    CS_FLASH = 1;
    CS_MAX   = 1;
}

/* -------- 8位收发:返回收到的字节 -------- */
unsigned char spi_xfer8(unsigned char out)
{
    unsigned char i, in = 0;

#if (SPI_MODE==0)
    for(i=0;i<8;i++){
        // 准备数据于 MOSI
        SPI_MOSI = (out & 0x80) ? 1 : 0; out <<= 1;
        spi_delay();
        // 上升沿:从机采样,主机随后在高电平期间读取 MISO
        SPI_SCK = 1; spi_delay();
        in = (in << 1) | (SPI_MISO ? 1 : 0);
        // 下降沿:准备下一位
        SPI_SCK = 0; spi_delay();
    }
#else   // SPI_MODE==3
    for(i=0;i<8;i++){
        // 空闲高,先在高电平准备数据
        SPI_MOSI = (out & 0x80) ? 1 : 0; out <<= 1;
        spi_delay();
        // 下降沿:从机采样
        SPI_SCK = 0; spi_delay();
        // 上升沿期间主机读取 MISO,回到空闲高
        SPI_SCK = 1; spi_delay();
        in = (in << 1) | (SPI_MISO ? 1 : 0);
    }
#endif
    return in;
}

/* 批量发送/接收(可按需使用) */
void spi_write_buf(const unsigned char *buf, unsigned int len){
    while(len--) (void)spi_xfer8(*buf++);
}
void spi_read_buf(unsigned char *buf, unsigned int len, unsigned char fill){
    while(len--) *buf++ = spi_xfer8(fill);
}

/* 简易毫秒延时(演示) */
void delay_ms(unsigned int ms){
    unsigned int i,j;
    for(i=0;i<ms;i++) for(j=0;j<125;j++);
}

5. 应用一:W25Qxx SPI Flash(读 JEDEC ID、读写示例)

5.1 指令与要点

  • JEDEC ID:0x9F → 返回 Manufacturer、MemoryType、Capacity(常见 W25Q32:EF 40 16)
  • 读数据:READ(0x03) + 24bit地址 + 连续数据
  • 写使能:WREN(0x06);写入/擦除前需要
  • 写状态/忙等待:RDSR(0x05)SR&0x01 为 BUSY
  • 页编程:PP(0x02),最多 256 字节,需落在同一页内
  • 扇区擦除:SE(0x20),4KB,耗时较长(>100ms)

注意:写/擦会修改 Flash 内容,演示时谨慎。示例默认只读 ID,可选打开写入测试。

5.2 完整代码(含 UART 打印)

/* ---------- 串口 9600 ---------- */
void uart_init(void){
    TMOD |= 0x20; TH1=0xFD; TL1=0xFD; TR1=1;
    SCON = 0x50; EA=1; ES=0;
}
void putc(char c){ SBUF=c; while(!TI); TI=0; }
void puts(const char* s){ while(*s) putc(*s++); }
void put_hex8(unsigned char v){
    const char hx[]="0123456789ABCDEF";
    putc(hx[(v>>4)&0xF]); putc(hx[v&0xF]);
}

/* ---------- W25Q 指令 ---------- */
#define CMD_RDID   0x9F
#define CMD_RDSR   0x05
#define CMD_WREN   0x06
#define CMD_READ   0x03
#define CMD_PP     0x02
#define CMD_SE     0x20

static void flash_select(void){ CS_FLASH = 0; }
static void flash_deselect(void){ CS_FLASH = 1; }

/* 读状态寄存器 */
unsigned char w25q_read_status(void){
    unsigned char sr;
    flash_select();
    (void)spi_xfer8(CMD_RDSR);
    sr = spi_xfer8(0xFF);
    flash_deselect();
    return sr;
}
void w25q_wait_busy(void){
    while(w25q_read_status() & 0x01){ /* BUSY */ }
}
void w25q_write_enable(void){
    flash_select();
    (void)spi_xfer8(CMD_WREN);
    flash_deselect();
}

/* 读 JEDEC ID:3字节 */
void w25q_read_jedec_id(unsigned char id[3]){
    flash_select();
    (void)spi_xfer8(CMD_RDID);
    id[0] = spi_xfer8(0xFF);
    id[1] = spi_xfer8(0xFF);
    id[2] = spi_xfer8(0xFF);
    flash_deselect();
}

/* 读取任意地址数据(len ≤ 256 仅示例) */
void w25q_read(unsigned long addr, unsigned char *buf, unsigned int len){
    flash_select();
    (void)spi_xfer8(CMD_READ);
    (void)spi_xfer8((addr>>16)&0xFF);
    (void)spi_xfer8((addr>>8)&0xFF);
    (void)spi_xfer8(addr&0xFF);
    while(len--) *buf++ = spi_xfer8(0xFF);
    flash_deselect();
}

/* 扇区擦除(4KB) */
void w25q_erase_sector(unsigned long addr){
    w25q_write_enable();
    flash_select();
    (void)spi_xfer8(CMD_SE);
    (void)spi_xfer8((addr>>16)&0xFF);
    (void)spi_xfer8((addr>>8)&0xFF);
    (void)spi_xfer8(addr&0xFF);
    flash_deselect();
    w25q_wait_busy();
}

/* 页编程(最多256字节,同页内) */
void w25q_page_program(unsigned long addr, const unsigned char *buf, unsigned int len){
    if(len==0) return;
    if(len>256) len=256;
    w25q_write_enable();
    flash_select();
    (void)spi_xfer8(CMD_PP);
    (void)spi_xfer8((addr>>16)&0xFF);
    (void)spi_xfer8((addr>>8)&0xFF);
    (void)spi_xfer8(addr&0xFF);
    while(len--) (void)spi_xfer8(*buf++);
    flash_deselect();
    w25q_wait_busy();
}

/* ---------- 演示主程序:读ID + 读/写测试 ---------- */
void demo_w25q(void){
    unsigned char id[3];
    unsigned char buf[32];
    unsigned char i;

    puts("W25Q JEDEC ID: ");
    w25q_read_jedec_id(id);
    put_hex8(id[0]); putc(' ');
    put_hex8(id[1]); putc(' ');
    put_hex8(id[2]); putc('\r'); putc('\n');

    /* 只读测试:读取 0x000000 开头32字节 */
    w25q_read(0x000000, buf, 16);
    puts("READ[000000]: ");
    for(i=0;i<16;i++){ put_hex8(buf[i]); putc(' '); }
    puts("\r\n");

    /* 可选:写入测试(注意会擦除扇区!谨慎开启) */
#if 0
    static const unsigned char msg[] = "Hello,51SPI!";
    puts("ERASE SECTOR @0x000000...\r\n");
    w25q_erase_sector(0x000000);
    puts("PROGRAM PAGE...\r\n");
    w25q_page_program(0x000000, msg, sizeof(msg)-1);
    w25q_read(0x000000, buf, sizeof(msg)-1);
    puts("VERIFY: ");
    for(i=0;i<sizeof(msg)-1;i++){ putc(buf[i]); }
    puts("\r\n");
#endif
}

6. 应用二:MAX7219 数码管/点阵驱动(SPI 串行)

MAX7219 通过 3 线 SPI(DIN/CLK/CS)串行装载 16 位命令,常用于 8×8 点阵或 8 位数码管。初始化后写显示寄存器即可点亮。

6.1 寄存器概览

  • 0x09 Decode Mode:0xFF=全位译码(适合数码管),0x00=不译码(适合点阵)
  • 0x0A Intensity:0x00~0x0F 亮度
  • 0x0B Scan Limit:0x07 显示8位
  • 0x0C Shutdown:0x01 正常工作,0x00 关机
  • 0x0F Display Test:0x01 测试全亮,0x00 正常显示
  • 0x01..0x08 Digit1…Digit8 显示数据

6.2 代码实现

/* 复用上面的 SPI 引脚与 CS_MAX */

static void max7219_write(unsigned char reg, unsigned char dat){
    CS_MAX = 0;
    (void)spi_xfer8(reg);
    (void)spi_xfer8(dat);
    CS_MAX = 1;
}

void max7219_init_numtube(void){
    max7219_write(0x09, 0xFF);  // 全位译码(0~F 的七段码)
    max7219_write(0x0A, 0x08);  // 亮度
    max7219_write(0x0B, 0x07);  // 8位扫描
    max7219_write(0x0C, 0x01);  // 退出关机
    max7219_write(0x0F, 0x00);  // 退出测试
    // 清屏
    for(unsigned char d=1; d<=8; d++) max7219_write(d, 0x0F); // 显示空白
}

void demo_max7219_count(void){
    unsigned int cnt=0;
    while(1){
        unsigned int v = cnt;
        // 逐位写入(低位在 Digit1)
        for(unsigned char d=1; d<=8; d++){
            max7219_write(d, v%10);
            v/=10;
        }
        cnt++;
        delay_ms(200);
    }
}

若连接的是 8×8 点阵(不译码),将 0x09 设为 0x00,然后往 0x01..0x08 写每行的 8 位点图即可。


7. 硬件 SPI 简述(以 STC8 为例)

增强型 51(STC8/12/15 等)内置 SPI 控制器,配置思路如下(寄存器名以 STC8H 为例,具体以芯片手册为准):

  • 使能 SPI 时钟、选择主模式、配置时钟极性相位(CPOL/CPHA)、波特分频;
  • 配置引脚映射(部分型号支持可选引脚/复用映射);
  • 发送:
    • 将字节写入 SPDAT,等待 SPIF 置位(或中断),读取 SPDAT 获得回读字节,清除 SPIF
  • 常用寄存器字段:
    • SPCONMSTR 主机、CPOL/CPHASPR1..0 分频、SSIG
    • SPSTATSPIF 完成标志、WCOL 冲突标志
    • SPDAT:数据寄存器

伪代码(仅示意):

void spi_hw_init_stc8_mode0(void){
    // 配置管脚复用 ...
    SPCON = 0x10  /*MSTR*/ | 0x00 /*CPOL=0*/ | 0x00 /*CPHA=0*/ | 0x03 /*分频*/;
    SPSTAT = 0xC0; // 清标志
}
unsigned char spi_hw_xfer8(unsigned char out){
    SPDAT = out;
    while((SPSTAT & 0x80)==0); // 等待SPIF
    SPSTAT = 0xC0;             // 清SPIF/WCOL
    return SPDAT;              // 回读
}

使用硬件 SPI 后,仅需把上层 spi_xfer8 映射为硬件实现,应用代码基本无需改动,效率显著提升。


8. 常见问题与排查

  • 无响应/全 0xFF:
    • CS 未正确拉低/拉高;MOSI/MISO 接反;从设备未上电或电平不兼容(3.3V/5V)。
  • 读值错位/花屏:
    • 模式不匹配(Mode0/Mode3);时钟过快;线太长耦合噪声。
  • 写 Flash 失败:
    • 未 WREN;忙等待不充分;跨页写;未擦除直接写导致校验失败。
  • MAX7219 不亮:
    • 未退出关机(0x0C=1);SCAN LIMIT 非 7;Decode 模式与硬件类型(数码管/点阵)不匹配。

9. 主程序整合示例

void main(void){
    uart_init();
    spi_gpio_init();

    puts("SPI Demo Start\r\n");

    /* 演示一:读取 W25Qxx JEDEC ID */
    demo_w25q();

    /* 演示二:MAX7219 计数显示(若已接模块) */
    max7219_init_numtube();
    demo_max7219_count();

    while(1);
}

按照你的硬件实际情况,二选一或都运行(若只接了其中一个模块,可将另一个演示注释)。


10. 小结

  • 实现了可复用的软件 SPI(Mode0/Mode3),便于大多数 51 工程快速接入 SPI 外设;
  • 给出两个高实用度示例:W25Qxx 读取 JEDEC ID、MAX7219 数码管显示;
  • 简述 STC8 硬件 SPI 的基本配置思路,便于后续提速与工程化。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐