尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

MCP23S17与STC12C5A32S2硬件SPI扩展16路IO实战

MCP23S17与STC12C5A32S2硬件SPI扩展16路IO实战 简介一套以STC12C5A32S2为主控、通过SPI协议驱动MCP23S17扩展I/O引脚的嵌入式开发工程包适合正在学习8051单片机、SPI通信及并行I/O扩展的开发者参考。工程文件完整共23个文件包含MCP23S17的C语言驱动源码、引脚定义头文件、UART调试程序、启动汇编文件、编译生成的HEX固件以及Keil工程配置整体仅28KB结构简洁易于移植。已有215人学习浏览具备一定参考价值。该工程不仅展示了从SPI初始化、MCP23S17寄存器配置到读写扩展端口的完整控制逻辑还通过串口输出相关调试信息方便实时观察扩展I/O状态变化。MCP23S17本身支持16位双向I/O配合中断与地址级联工程中给出了基本读写示例开发者可直接烧录体验也可基于源码深入理解两芯片间的通信时序并根据实际需求修改引脚映射或工作模式是学习单片机IO扩展和SPI从设备驱动的实用范例。1. 一块MCP23S17让STC12C5A32S2多出16个IO这条路为什么值得走用MCP23S17搭配STC12C5A32S2先讲一个反直觉的结论8051平台的SPI并不慢慢的是那些用软件模拟、一个位一个位翻转的写法。STC12C5A32S2内部带硬件SPI模块配合MCP23S17这颗16位I/O扩展芯片4根线就能换来16个可独立输入或输出的端口而且读写吞吐比74HC595这类串行移位芯片高一截。很多工程师在成品板上遇到IO不够的问题第一反应是换更大封装的单片机或者再接一块595只做输出。但MCP23S17能双向操作、支持中断还能同时驱动LED和读取按键很适合在保留现有8051设计的前提下把端口扩出来。这篇内容从寄存器模型讲起落到STC12C5A32S2的SPI配置和Keil C51驱动代码最后给一份现场排错清单和两个进阶技巧。适合正在做仪表、控制器、或者旧板子改造手头有8051经验但第一次碰MP23S17的人。下面所有代码都按最小系统来写拿到手改引脚定义就能用。2. MCP23S17的寄存器模型与STC12C5A32S2的SPI外设先对准这两张表不管你在MDK5的8051工程还是IAR 6.3的8051开发环境里写代码先把MCP23S17和STC12C5A32S2的“翻译规则”对齐否则后面全是乱码。MCP23S17靠一个命令字节决定读写对象和寄存器地址STC12C5A32S2靠SPI控制寄存器的几个位决定时钟波形。两边任何一个位不对读回来的数据都会错而且错得非常随机。2.1 MCP23S17的地址引脚A0/A1/A2决定命令字节是0x40还是0x48MCP23S17的从机地址由硬件引脚A0、A1、A2决定固定高四位是0100。比如三个引脚全部接地地址就是0100000等于0x20。SPI写命令 地址左移1位最后一位补0所以是0x40读命令 地址左移1位最后一位补1所以是0x41。如果A2接了高电平从机地址变成0x24写命令就变成0x48读命令变成0x49。这个地址问题是最隐蔽的坑很多例程只写了0x40如果你的板子上A0-A2不是全接地发给芯片的所有命令都会被忽略初始化函数推过去没有任何反应。我一般会建议在PCB上把A0-A2做成跳线默认拉地调试时就算改了地址也不用动代码。另外要注意MCP23S17有一个IOCON寄存器的HAEN位bit3默认是0此时硬件地址不参与译码不管A0-A2怎么接都只认0x40/0x41。想用多颗MCP23S17挂在同一条SPI总线上就必须先把HAEN置1再用不同的A0-A2组合区分。2.2 控制寄存器表IODIR、GPIO、IOCON是最常用的三个MCP23S17每个端口PORTA和PORTB都有对应的方向、极性、中断、输出寄存器但实际调试时你只需要先吃透下面这三组寄存器名PORTA地址PORTB地址作用IODIR0x000x010表示输出1表示输入GPIO0x120x13写入输出状态读入引脚电平IOCON0x0A0x0B控制地址使能、连续操作、银行模式IODIR寄存器是核心。想要某个引脚输出就把对应位写成0想读按键就把对应位写成1。GPIO寄存器在输出模式下写入锁存值在输入模式下读回当前引脚电平。IOCON里有两个容易忽略的位HAENbit3前面说过了控制硬件地址译码SEQOPbit5控制寄存器地址连续增加置0时SPI每传完一个字节内部寄存器地址自动加1这在连续写16个输出时非常有用后面第5章会用到。注意MCP23S17还分“I/O银行”模式默认是FIFO模式PORTA和PORTB的寄存器分开编址我们的驱动都按这个模式写。如果你把IOCON的BANK位置1寄存器地址表会变成两套网上很多零散代码的地址对不上多半就是银行模式不同。2.3 STC12C5A32S2的SPI控制位CPOL/CPHA和SPR0/SPR1怎么配STC12C5A32S2的SPI主机模式由SPCTL寄存器管理常用位是SSIG忽略SS引脚、SPENSPI使能、MSTR主机模式、CPOL时钟极性、CPHA采样相位、SPR1和SPR0分频。MCP23S17支持SPI模式0和模式3也就是CPOL0/CPHA0或者CPOL1/CPHA1。实际电路里我习惯配成模式0空闲时钟为低、第一个上升沿采样。分频先设为系统时钟的4分频跑通之后再把SPR位改成2分频提高速度不要一开始就拉满否则毛刺干扰会让你误判是芯片坏了。下面这段初始化代码就是把STC的SPI设成主机、模式0、4分频void stc_spi_init(void) { SPCTL 0xE0; /* 0b11100000 */ // bit7 SSIG1: 忽略SS引脚CS由普通GPIO手动控制 // bit6 SPEN1: SPI模块使能 // bit5 MSTR1: 主机模式 // bit4 CPOL0, bit3 CPHA0: SPI模式0 // bit2/bit1 0: 系统时钟4分频 SPSTAT 0xC0; /* 清除SPIF和WCOL标志 */ }这里把SSIG置1很关键。STC12C5A32S2的硬件SPI默认会检测P1.4上的SS电平如果不忽略SS脚一旦被拉低SPI可能自动切到从机模式。我们用普通GPIO控制MCP23S17的CS所以必须让SSIG1。SPSTAT写入0xC0是为了清掉上电残留的中断标志两个标志位都是写1清零。不同型号的STC12头文件里SPCTL、SPSTAT的字节地址略有区别但位含义一致在MDK5或IAR 6.3里只要头文件选对代码可以直接复用。3. 在Keil C51里写MCP23S17驱动硬件连接、初始化和读写函数有了第2章的寄存器基础接下来就是动手接线和写代码。这一章的代码可以直接抄进一个新建的C文件前提是你的单片机已经把SPI引脚复用功能打开。STC12C5A32S2的SPI引脚固定在P1.5到P1.7我见过有人试图把SCK换到P2口那是做不到的除非完全用软件模拟SPI但那套代码和这里的硬件SPI驱动不通用。3.1 硬件连接表CS别用SS引脚RST用电阻拉高MCP23S17与STC12C5A32S2的标准连接方式如下STC12C5A32S2引脚MCP23S17引脚说明P1.5SCKSPI时钟P1.6SOSPI从机输出主机读P1.7SISPI从机输入主机写P1.4普通GPIOCS片选软件拉低有效3.3V或5VVDD注意电平匹配GNDVSS共地任意IO或复位电路RESET必须拉高不能悬空CS不要接到STC的SS功能脚P1.4上没问题但要把P1.4配置成普通输出IO。因为SSIG已经是1P1.4不会触发SPI从机切换你可以放心用。RESET引脚要接一个10kΩ电阻到VDD否则上电瞬间MCP23S17可能进入未知状态。电平方面STC12C5A32S2是宽电压器件MCP23S17也有5V耐压版本如果两边都是3.3V供电就直连如果MCP23S17用5V、STC用3.3VSI和SCK之间要串分压电阻或者用电平转换芯片。3.2 初始化代码先设SPI再清MCP23S17的方向寄存器#include STC12C5A32S2.H #include intrins.h #define MCP23S17_CS P1_4 void mcp23s17_write_reg(unsigned char addr, unsigned char value) { MCP23S17_CS 0; SPI_RW(0x40); /* 写命令地址0x002位 */ MCP23S17_CS 0; SPI_RW(addr); /* 寄存器地址 */ SPI_RW(value); /* 要写入的数据 */ MCP23S17_CS 1; }上面这段代码里的SPI_RW是发送一个字节并同时读回一个字节的底层函数通常直接用SPDAT寄存器实现。注意MCP23S17写寄存器必须一次发三个字节命令字节、寄存器地址、数据。很多人只发两个字节结果写入失败。这里CS的拉低放在命令字节之前直到数据发送完后才拉高不能每发一个字节就拉高一次不然MCP23S17会认为是三笔独立传输。3.3 读寄存器函数先写地址再读回数据CS保持低电平unsigned char mcp23s17_read_reg(unsigned char addr) { unsigned char result; MCP23S17_CS 0; SPI_RW(0x41); /* 读命令 */ SPI_RW(addr); /* 写入要读的寄存器地址 */ result SPI_RW(0x00); /* 发送任意数据读回结果 */ MCP23S17_CS 1; return result; }读操作同样需要先发命令和地址之后还需要再发一个字节来触发SPI时钟才能在MISO上读回数据。这里的第三个字节可以随意写很多例程写成0x00。SPI是全双工协议无论读还是写主机都要产生时钟这个“假写”是必须的。如果你发现读回来的永远是0x00先检查第三字节有没有发出去以及CS是不是在整个读过程中一直保持低电平。我调试时踩过这个坑把CS在发完地址后提前拉高结果读回来的全是0xFF。3.4 完整初始化让所有IO默认输出低电平void mcp23s17_init(void) { stc_spi_init(); mcp23s17_write_reg(0x0A, 0x20); /* IOCON: SEQOP0, HAEN0, BANK0 */ mcp23s17_write_reg(0x00, 0x00); /* IODIRA: 全输出 */ mcp23s17_write_reg(0x01, 0x00); /* IODIRB: 全输出 */ mcp23s17_write_reg(0x12, 0x00); /* GPIOA: 输出低电平 */ mcp23s17_write_reg(0x13, 0x00); /* GPIOB: 输出低电平 */ }这里IOCON写入0x20二进制是00100000bit5是SEQOP置0允许地址递增。虽然当前初始化还没用连续写但提前把这个位设好后面第5章的连续更新16个输出就直接有效。IODIR和GPIO的地址用PORTA的因为默认FIFO模式下地址连续。上面的初始化顺序是固定流程先把IOCON设好再设方向寄存器最后给输出寄存器一个初值。如果顺序反了IO口在上电瞬间可能输出不确定电平驱动外部继电器时会出现误动作。4. 让MCP23S17稳定工作的排错清单从地址冲突到SPI波形硬件和驱动都写完板上不一定一次就通。MCP23S17和STC12C5A32S2的组合出问题一般集中在地址、SPI模式、复位这三个因素。下面这份清单按排查顺序排列每一条都对应一个我能想到的真实故障。4.1 回读IODIR寄存器能读回0xFF就是通信通了最有效的验证方法不是看LED而是读寄存器。方向寄存器IODIRA的地址是0x00把它的值先写成0xFF表示全输入再读回来如果读到0xFF说明SPI链路、CS控制、命令字节都正确。下面这段测试代码可以作为第一步unsigned char test_value; mcp23s17_write_reg(0x00, 0xFF); /* IODIRA全输入 */ test_value mcp23s17_read_reg(0x00); if (test_value 0xFF) ; /* 通信正常 */ else ; /* 检查接线或地址 */如果读回的值不是0xFF先核对你的命令字节是不是0x40/0x41再检查A0-A2是否和你代码里期望的一致。如果读回值漂浮不定用示波器看MCP23S17的SCK脚有没有时钟SI脚有没有正确的电平变化。没有示波器的话也可以把SPI分频调到系统时钟16分频降低速度后故障会更容易暴露。4.2 CPOL/CPHA配错的典型症状错位又不全错SPI模式不匹配的表现非常磨人有时候连续读两个字节第一个对、第二个错或者读写低字节对、高字节错。原因在于MCP23S17在时钟边沿采样如果你的CPHA晚了一个边沿数据就会被移错半位。STC12C5A32S2的SPI模式0和模式3都能配合MCP23S17但必须保证CPOL和CPHA一致。很多从别的平台移植过来的代码喜欢用模式3那你就得把SPCTL里的CPOL置1、CPHA置1不能照抄我的0xE0。改法是把0xE0改成0xD0CPOL1或0xF0CPOL1, CPHA1具体看你的习惯。4.3 RESET引脚和上电顺序MCP23S17没有软件复位MCP23S17不像一些新芯片有软件复位命令它只能靠RESET引脚硬复位。这个引脚上电后必须处于高电平。如果你的电路里RESET悬空芯片可能工作一段时间后内部状态乱掉表现为IO输出锁存值能写但读回来不对。稳妥做法是把RESET接到STC12C5A32S2的一个普通IO口上初始化时先拉低至少10us再拉高明确让MCP23S17从已知状态开始。需要说明的是MCP23S17的RESET是低有效我推荐在初始化函数第一步加几行代码RESET_PIN 0; _nop_(); _nop_(); RESET_PIN 1; delay_ms(1);这样比单纯电阻拉高多了软件可控性排查现场问题时也能随时把扩展芯片复位一次。如果RESET被复用成其他输出注意不要在MCP23S17读写期间触发复位否则正在传输的SPI事务会被打断。5. 用连续写和中断线把MCP23S17用满两个可以直接搬的代码最后这两个技巧属于“把已经能用的东西再往前顶一步”。第一种是连续写模式一次SPI事务更新16个输出第二种是把MCP23S17的中断输出和STC12C5A32S2的外部中断结合避免轮询按键。两个在真实产品里都很实用。5.1 连续写模式SEQOP置0后一次事务写满GPIOA和GPIOB当IOCON的SEQOP为0时MCP23S17会在每次SPI字节传输后自动递增寄存器地址。如果我们连续发送GPIOA地址0x12再依次发两个数据字节第二个数据就会自动写入GPIOB。这样可以只拉高取消一次CS就更新16路输出效率比两次写寄存器更好。下面是示例void mcp23s17_write_all(unsigned char high, unsigned char low) { MCP23S17_CS 0; SPI_RW(0x40); /* 写命令 */ SPI_RW(0x12); /* GPIOA地址 */ SPI_RW(low); /* PORTA输出 */ SPI_RW(high); /* 地址自动递增写入PORTB */ MCP23S17_CS 1; }注意这里的high和low顺序要和你的硬件连接对应。PORTA的第0位对应MCP23S17的引脚0如果硬件图上PORTA接的是低位LED就把低位参数放前面。连续写时CS在整个事务中必须保持低不能中途拉高否则地址递增会中断。这种方法特别适合跑点阵刷新或步进电机换相一次事务减少一半的开销。5.2 用INTB下降沿做按键扫描把输入端口从轮询变成中断触发MCP23S17的INTPB引脚可以在PORTB输入电平变化时输出下降沿脉冲。把它接到STC12C5A32S2的外部中断0通常是P3.2上就能让CPU从持续读端口的忙等中解放出来。配置方法先设IODIRB为全输入IPOLB不修改GPINTENB置1使能中断再设DEFVALB和INTCONB为0这样只要输入电平相对上一次变化就会触发中断。中断服务函数里读回GPIOB清除中断标志代码如下void ext0_isr(void) interrupt 0 { unsigned char key_state; key_state mcp23s17_read_reg(0x13); /* 读GPIOB同时清中断 */ /* 根据key_state做按键处理 */ }读GPIOB本身就是清中断动作不需要额外操作MCP23S17的中断标志寄存器。由于INTB通过外部中断读取还要注意MCP23S17的中断输出在MISO引脚上也有可能复用但STC的硬件SPI模式里MISO是输入没有冲突。如果你的按键按下时间较长中断服务函数里不要再做延时消抖直接记录状态主循环里再判断延时否则会阻塞外部中断。最后提醒一句连续写和中断功能都依赖第2章说的IOCON寄存器如果你把IOCON写成默认值0x00SEQOP是1上面的所有连续操作会失败第5.1节的4字节事务里第三个字节会被当作另一个寄存器地址而不是数据。本文还有配套的精品资源点击获取
返回列表