用3个GPIO扩展16路串行输出?揭秘移位寄存器的硬核玩法
你有没有遇到过这样的窘境:项目做到一半,MCU的UART口全占满了,GPIO也快捉襟见肘,可功能还得加?换大芯片?成本飙升。加协处理器?PCB空间不够。这时候,一个被很多人“看不上”的小器件——74HC595移位寄存器,反而可能成为破局的关键。
别急着划走。这可不是什么教科书里的老古董,而是一招在真实工业设计中屡试不爽的“以软补硬”神技。今天我们就来拆解:如何用一片几毛钱的移位寄存器,让只有1个串口的MCU,同时控制8个外设,甚至模拟出多路“虚拟串口”。
为什么你会缺串口?现实比想象更骨感
我们总以为MCU有几个UART就够了,但现实是:
- 你要接Wi-Fi模块(占1路)
- 要连蓝牙模组(再占1路)
- 调试需要打印日志(又1路)
- 外部传感器还要通信……
结果呢?STM32F030、ESP32-C3这类高性价比芯片,往往只配1~2个UART。想靠原生接口全接上?门都没有。
更头疼的是,很多“通信”其实根本不需要真正的UART。比如:
- 给数码管发段码
- 控制继电器开关
- 驱动LED点阵
这些操作本质是发送一串并行数据,却因为没引脚,被迫占用宝贵的串口资源。这就有点“杀鸡用牛刀”了。
那有没有办法把“通用IO”变成“准串口”?有——软件模拟 + 移位寄存器打辅助。
移位寄存器不是IO扩展芯片,它是“串行流搬运工”
先澄清一个常见误解:74HC595本身不是串口,它是个“串行输入、并行输出”的数据转接器。你可以把它想象成一条8节车厢的货运列车:
- MCU是调度员,一位一位地把货物(bit)送上车;
- 每来一个时钟脉冲,列车就向前挪一格,把新货装进第一节,其他货依次后移;
- 8位装满后,按下“发车键”(锁存信号),整列车的货物瞬间卸到8个目的地(Q0~Q7)。
整个过程只需要3根线:
-DATA:数据线(送货)
-CLK:时钟线(推进车厢)
-LATCH:锁存线(一键卸货)
就这么简单,8位并行输出到手,成本不到1毛钱。
关键洞察:移位寄存器的价值不在“扩展IO”,而在于把时间维度上的串行操作,转化为空间上的并行控制。这才是它能“扩展串口”的底层逻辑。
真正的妙招:用移位寄存器模拟“多路串行发送”
到这里,很多人以为这就是终点——不就是驱动LED吗?但高手的玩法才刚开始。
设想这样一个需求:你要向4个独立设备广播命令,比如“RELAY_ON”、“DISPLAY_H”、“SENSOR_RESET”……每个设备都通过单独的使能脚触发。传统做法是用4个GPIO分别控制,或者用I2C IO扩展。
但我们换个思路:能不能把这4个控制线当成“4路虚拟串口”的TX线?
答案是可以。只要我们配合定时器,就能实现一种叫“位时间轮询 + 移位寄存器同步输出”的机制。
核心思路:把“位”当成“通道”
假设我们设定波特率为9600bps,每一位持续约104μs。我们可以这样安排:
- 开一个定时器中断,每104μs触发一次;
- 在每次中断里,检查4个虚拟串口当前该发哪一位;
- 把这4位拼成一个字节(bit0对应通道0,bit1对应通道1……);
- 用
shiftOut()把这个字节一次性推给移位寄存器; - 锁存更新,4路输出同步刷新。
你看,原本需要4次独立的GPIO操作,现在被压缩成一次“打包发送”。MCU不再忙于切换引脚,而是由移位寄存器统一承载输出状态。
这就像从“4个快递员各自送货”升级为“1辆厢式货车集中配送”,效率自然提升。
实战代码:一个多通道软件UART发送器
下面这段代码跑在STM32上,实现了4路独立的串行发送通道,仅占用3个GPIO和一个定时器:
#define CHANNEL_COUNT 4 #define BUFFER_SIZE 16 typedef struct { uint8_t buffer[BUFFER_SIZE]; uint8_t head; uint8_t tail; uint8_t busy; } SerialChannel; SerialChannel channels[CHANNEL_COUNT]; // 当前正在发送的位索引(0~9:起始+8数据+停止) static uint8_t bit_pos[CHANNEL_COUNT] = {0}; // 当前待发送字节缓存 static uint8_t tx_data[CHANNEL_COUNT] = {0}; void TIM2_IRQHandler(void) { if (TIM2->SR & TIM_SR_UIF) { TIM2->SR &= ~TIM_SR_UIF; // 清除中断标志 uint8_t output_byte = 0; for (int ch = 0; ch < CHANNEL_COUNT; ch++) { if (!channels[ch].busy) continue; uint8_t current_bit = 0; if (bit_pos[ch] == 0) { current_bit = 0; // 起始位 = 0 } else if (bit_pos[ch] <= 8) { current_bit = (tx_data[ch] >> (bit_pos[ch]-1)) & 0x01; } else { current_bit = 1; // 停止位 = 1 } if (current_bit) { output_byte |= (1 << ch); // 第ch位设为高 } bit_pos[ch]++; if (bit_pos[ch] >= 10) { bit_pos[ch] = 0; channels[ch].busy = 0; send_next_byte(ch); // 启动下一字节 } } shiftOut(output_byte); // 所有通道状态一次性输出 } }配合前面提供的shiftOut()函数,这个系统就能稳定地以9600bps向4个设备发送自定义指令。如果你想扩展到8路?换一片74HC595级联就行,代码几乎不用改。
工程实测:一个小面板的大变身
我在一个工业温控面板中实际应用了这套方案。主控是STM32F030K6T6,QFN28封装,总共才28个引脚,原生1个UART。
原始需求:
- 接ESP-01S Wi-Fi模块(必须用UART)
- 驱动4位数码管(需8段+4位选 = 12 GPIO)
- 控制8路继电器(8 GPIO)
- 保留SWD下载口(2 GPIO)
- 还要留几个给按键和传感器……
算下来至少需要24个GPIO,刚好撞墙。
引入双级联74HC595后,架构变成:
[STM32] ├── UART0 → ESP-01S ├── PA0 → DATA ├── PA1 → CLK ├── PA2 → LATCH └── [74HC595 × 2] ├── Q0~Q7 → 数码管段 a~dp └── Q8~Q15 → 继电器 IN1~IN8总共只用了3个额外GPIO,省下13个引脚!
而且数码管显示和继电器控制还能共用同一套发送机制。比如我要显示“H”并打开第3路继电器,只需构造两个字节:
uint8_t seg_code = 0b01110110; // "H" 字形 uint8_t rel_code = 0b00000100; // 第3路闭合 shiftOut(seg_code); shiftOut(rel_code); HAL_GPIO_WritePin(GPIOA, LATCH_PIN, GPIO_PIN_SET); // 更新输出干净利落,毫无压力。
别忽视这些细节,它们决定成败
你以为接上线就能跑?工程远没那么简单。以下是我在调试中踩过的坑:
1. 电源噪声导致乱码
移位寄存器在切换瞬间电流突变,容易拉低VCC。务必在每片旁边加0.1μF陶瓷电容,离电源引脚越近越好。我曾因省掉这个电容,导致数码管频繁闪屏。
2. 锁存信号太“脆”
如果LATCH信号抖动,可能在移位中途触发锁存,造成半截数据显示。建议:
- 在shiftOut()末尾加短延时;
- 或使用硬件逻辑门对LATCH做滤波;
- 更稳妥的做法是在初始化时将其默认拉高。
3. 级联顺序搞反了
74HC595的数据是从Q7’(MSB)输出的,如果你级联时接错了方向,高低位就会颠倒。记住口诀:“前片尾巴接后片头”。
4. 高频下时序不够用
如果你目标是模拟115200bps,那每位只有约8.7μs。在慢速MCU上,shiftOut()函数本身可能就要几微秒,留给其他任务的时间极短。建议:
- 使用DMA+SPI方式替代纯软件模拟;
- 或选用更快的74AC系列芯片(支持70MHz时钟)。
它不适合什么场景?
当然,这招也不是万能的。以下情况慎用:
- 需要接收数据:本方案主要是发送。若需双向通信,得另加比较器或ADC采样;
- 实时性要求极高:如电机控制中的PWM同步,这种轮询机制会有微秒级延迟;
- 长距离传输:直接输出易受干扰,建议加光耦隔离后再走线;
- 多人协作项目:新手可能看不懂这种“非标准”设计,增加维护成本。
但在大多数IoT节点、人机交互面板、状态指示类设备中,它的性价比简直无敌。
写在最后:老器件的新生命
74HC595诞生于上世纪80年代,但它从未过时。在RISC-V MCUs遍地开花、国产逻辑芯片批量替代的今天,这种“软硬协同”的极简设计思想反而更加珍贵。
它教会我们的不只是技术,更是一种思维:当硬件资源见底时,不妨回头看看那些最基础的元件——有时候,解决问题的钥匙,就藏在最不起眼的地方。
如果你也在为引脚发愁,不妨试试这片几毛钱的芯片。说不定,你的下一个产品,就靠它“续命”成功。
对了,评论区聊聊:你用移位寄存器做过最骚的操作是什么?我听过有人拿它做简易DAC,还有人用16级联点亮了8×8 LED矩阵……