51单片机基础-软件SPI 及其简单应用
本文介绍了51单片机实现SPI通信及其应用的方法。主要内容包括:1) SPI硬件设计要点,如四线连接方式和电平匹配;2) 通过软件模拟实现通用SPI驱动,支持Mode0/Mode3两种模式切换;3) 两个典型应用示例:读取W25Qxx闪存的JEDEC ID和使用MAX7219驱动数码管显示;4) 提供完整代码实现,包含SPI初始化、数据传输函数和Flash操作指令。文章特别强调了5V/3.3V电平
第二十八章 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 → SCK,P1.4 → MOSI,P1.5 ← MISOP1.0 → CS_FLASH(W25Qxx),P1.1 → CS_MAX(MAX7219)
- 去耦:每个芯片 VCC-GND 近端放置 0.1µF;Flash 等对电源较敏感,可再并 10µF。
3. SPI 基础与模式
- 时序参数由
CPOL与CPHA决定,常用两种:- Mode 0:
CPOL=0, CPHA=0(空闲 SCK 低,数据在上升沿采样) - Mode 3:
CPOL=1, CPHA=1(空闲 SCK 高,数据在下降沿采样)
- Mode 0:
- 位序:大多数器件 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 寄存器概览
0x09Decode Mode:0xFF=全位译码(适合数码管),0x00=不译码(适合点阵)0x0AIntensity:0x00~0x0F 亮度0x0BScan Limit:0x07 显示8位0x0CShutdown:0x01 正常工作,0x00 关机0x0FDisplay Test:0x01 测试全亮,0x00 正常显示0x01..0x08Digit1…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;
- 将字节写入
- 常用寄存器字段:
SPCON:MSTR主机、CPOL/CPHA、SPR1..0分频、SSIG等SPSTAT:SPIF完成标志、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 的基本配置思路,便于后续提速与工程化。
更多推荐



所有评论(0)