
1. 项目缘起当RDK-S100的GPIO不够用时最近在折腾一块基于瑞芯微RK3588的RDK-S100开发板这板子性能是真不错跑AI推理、做边缘计算网关都绰绰有余。但玩着玩着一个很现实的问题就摆在了面前它的原生GPIO通用输入输出引脚数量对于稍微复杂一点的硬件项目来说有点捉襟见肘。你想接几个传感器、控制几个电机、再挂个显示屏和几个按键引脚很快就用完了。这时候一个MCU端口扩展板就成了刚需。这个“RDK-S100-MCU-Port-Expansion-Board”项目就是为解决这个问题而生的。它的核心思路很简单用一个成本低廉、资源丰富的微控制器MCU作为“中间人”通过高速串口如UART、SPI或I2C与RDK-S100主控通信。MCU负责管理大量的GPIO、ADC模数转换器、PWM脉冲宽度调制等外设而RDK-S100则通过发送简单的命令协议来间接控制这些扩展出来的硬件资源。这样一来主控的算力可以专注于复杂的应用逻辑和AI任务而繁琐的、实时的IO控制则交给更擅长此道的MCU去处理。这种架构在工业控制、机器人、智能家居中非常常见。比如你可能需要同时读取十几个传感器的数据或者精确控制多个舵机的角度这些任务对实时性和引脚数量要求高但逻辑不复杂用一块STM32、GD32或者ESP32这样的MCU来分担再合适不过。这个扩展板项目就是要为RDK-S100搭建这样一个可靠、高效的“手脚”。2. 核心方案选型为什么是MCU协议而不是纯逻辑芯片面对GPIO扩展的需求市面上其实有好几种方案。最简单粗暴的是使用74HC595这样的串入并出移位寄存器或者PCA9555这类I2C接口的IO扩展芯片。它们成本极低接线简单对于只需要简单数字输入输出的场景是够用的。但是它们的功能非常单一只能做最基本的电平读写无法处理模拟信号ADC、产生PWM波、或者处理复杂的通信协议如单总线、WS2812B灯带协议。另一种方案是使用CPLD或FPGA。它们功能强大且灵活可以实现任何你想要的逻辑性能也极高。但问题在于它们的开发门槛高需要硬件描述语言如Verilog的知识工具链复杂成本也相对较高。对于大多数嵌入式应用开发者来说有点“杀鸡用牛刀”的感觉。所以基于MCU的扩展方案成为了一个平衡点。它优势明显功能全面现代MCU集成了ADC、DAC、PWM、定时器、多种串行通信接口等丰富外设几乎是一个“瑞士军刀”。开发友好使用C/C等高级语言编程有成熟的IDE如Keil、IAR、ESP-IDF、Arduino、丰富的库和社区支持开发效率高。成本可控从几块钱的国产GD32到几十块的STM32选择范围广性价比高。分担主控压力MCU可以独立处理实时性要求高的任务如电机PID控制、传感器滤波仅将结果或高级指令上报给主控极大减轻了主控的负担和中断压力。在这个项目中我们与RDK-S100的通信协议是整个设计的核心。考虑到稳定性、速度和易用性我选择了基于UART的定制二进制协议而不是I2C或SPI。原因如下I2C虽然只需要两根线SDA SCL但在长距离、高干扰环境下稳定性不如UART且总线上挂载多个设备时需要地址管理速度也相对较慢标准模式100kbps快速模式400kbps。SPI速度最快全双工但需要4根线MOSI MISO SCK CS当需要扩展多个SPI设备时片选线CS会占用大量主控GPIO失去了扩展的意义。UART折中之选。只需要两根线TX RX即可实现全双工异步通信抗干扰能力比I2C强速度可以轻松达到921600bps甚至更高足以应对绝大多数扩展IO的指令和数据传输需求。协议设计也相对简单直观。因此本扩展板的硬件核心就是一块带有足够多GPIO和UART接口的MCU软件核心则是一套运行在MCU上的固件以及运行在RDK-S100Linux系统上的配套驱动/库。3. 硬件设计与关键元器件选型设计一块扩展板硬件是基础。这里我详细拆解各个部分的设计考量。3.1 MCU选型STM32F103C8T6 “蓝色药丸”虽然市面上MCU型号繁多但考虑到生态、成本和性能我最终选择了经典的STM32F103C8T6也就是大家常说的“Blue Pill”蓝色药丸开发板的核心芯片。选它有几个硬核理由生态无敌STM32的HAL库、标准库资料浩如烟海任何问题几乎都能找到答案。Arduino Core for STM32的支持也非常完善给快速原型开发提供了可能。资源够用它拥有64KB Flash20KB RAM37个GPIO3个USART2个SPI2个I2C3个12位ADC4个通用定时器可用于PWM。对于端口扩展板来说这些资源绰绰有余。成本低廉国产替代型号如GD32F103C8T6价格极具竞争力大批量采购单价可以控制在10元人民币以内。调试方便支持SWD调试配合ST-Link或J-Link可以方便地进行单步调试和固件下载。当然也有朋友会问“J-Flash里面没有所需要的MCU型号怎么办”这是使用J-Link工具时常见的问题。J-Flash的数据库可能没有收录一些较新或小众的型号。解决办法通常是去Segger官网下载最新的J-Flash软件包通常包含更新的设备支持包。手动添加设备。在J-Flash中你可以根据芯片的数据手册手动输入Flash大小、RAM大小、内存映射地址等关键参数来创建一个新的设备条目。对于STM32F103系列由于其普遍性最新版的J-Flash通常都自带支持。3.2 电平转换与通信接口RDK-S100的GPIO电平是3.3V而STM32F103虽然也是3.3V供电但为了确保通信稳定尤其是长距离连接时必须考虑电平匹配和电气隔离。直接连接在短距离内可能工作但不是稳健的设计。UART通信电路我们在MCU的USART1的TXPA9和RXPA10引脚上串联了22欧姆的电阻用于抑制信号反射。然后通过一个双向电平转换芯片如TXS0108E或74LVC4245与RDK-S100的UART引脚连接。这不仅确保了电平一致还提供了一定的ESD保护。如果对隔离有要求还可以增加ADM3251E这类隔离式UART收发器。调试接口板上引出了标准的SWD接口SWDIO SWCLK GND 3.3V用于连接ST-Link进行编程和调试。同时也通过CH340G这类USB转TTL芯片引出了一个Micro-USB口用于直接通过串口给MCU烧录固件或打印调试信息非常方便。扩展引脚排列将MCU剩余的GPIO、ADC引脚、5V/3.3V电源、GND等以2.54mm排针的形式整齐地排列在板子两侧。每个引脚都丝印了其功能如PA1 ADC1_IN1并尽可能将同一功能的外设如所有ADC引脚排列在一起方便接线。3.3 电源设计稳定的电源是系统可靠运行的基石。扩展板从RDK-S100上取电通常有5V和3.3V两种选择。输入我们从RDK-S100的GPIO排针上取5V输入。选择5V而不是3.3V是因为5V的电流承载能力通常更强且经过板载LDO降压到3.3V后纹波更小能为MCU提供更干净的电源。降压使用一颗AMS1117-3.3或性能更好的LD1117-3.3线性稳压器将5V降压为3.3V为整个MCU及周边电路供电。在输入和输出端都并联了10uF的钽电容和0.1uF的陶瓷电容以滤除高频和低频噪声。输出扩展板也将这路5V和3.3V电源输出到排针上供外部传感器、执行器使用。但需要特别注意总电流不能超过RDK-S100该5V引脚的最大输出能力通常建议在1A以内具体需查阅RDK-S100的硬件手册。3.4 PCB布局与布线注意事项画PCB时有几个坑我踩过这里分享出来退耦电容要紧贴MCU每个VDD和VSS引脚附近1cm以内的0.1uF退耦电容必须放到位这是保证MCU高速稳定运行的关键能提供瞬间的电流需求。晶振走线要短而直给MCU提供时钟的8MHz晶振以及32.768KHz的RTC晶振如果使用其走线要尽可能短远离高频信号线如SWD线并用地线包围以减少干扰。电源路径清晰电源从输入接口到LDO再到MCU的路径要粗、要直接。避免在数字信号区域下方走电源线防止噪声耦合。预留测试点在关键的电源节点5V_IN 3.3V_OUT、UART信号线、复位引脚等处预留出裸露的焊盘作为测试点方便后期用示波器或万用表进行测量和调试。4. 固件设计MCU端的“大脑”硬件是躯体固件才是灵魂。MCU端的固件需要稳定、高效地解析来自主控的命令并控制硬件。4.1 通信协议设计我们设计一个简单高效的二进制协议。每个数据包结构如下字节偏移字段长度说明0帧头1字节固定为0xAA用于帧同步1命令字1字节指示操作类型如0x01读GPIO0x02写GPIO0x03读ADC...2数据长度1字节后续数据域的长度N3 ~ 3N-1数据域N字节具体的参数如GPIO编号、电平值、ADC通道等3N校验和1字节从帧头到数据域最后一个字节的累加和取低8位举例主控发送命令让扩展板的PA1引脚输出高电平。命令字0x02 (写GPIO)数据域需要两个参数。第一个参数指定哪个引脚。我们可以定义一个映射表例如0x01代表PA1。第二个参数代表电平0x00为低0x01为高。假设数据域为[0x01 0x01]长度N2。计算校验和0xAA 0x02 0x02 0x01 0x01 0xB0。取低8位即0xB0。最终发送的字节序列为AA 02 02 01 01 B0MCU端固件需要实现一个状态机来解析这个协议等待帧头持续读取串口数据直到收到0xAA。接收命令与长度读取接下来的命令字和数据长度。接收数据域根据数据长度N读取N个字节的数据。验证校验和计算校验和并与接收到的校验和字节比较。一致则执行命令不一致则丢弃该帧并返回错误码。执行与响应执行相应操作如设置GPIO然后组织一个响应数据包包含执行状态、读取的数据等发送回主控。4.2 外设驱动封装为了便于管理我们将对GPIO、ADC、PWM等操作封装成统一的函数接口供协议解析器调用。// gpio_controller.c typedef enum { PORT_A PORT_B PORT_C // ... 其他端口 } GPIO_Port; typedef struct { GPIO_Port port; uint16_t pin; } GPIO_PinDef; // 初始化所有扩展GPIO为输入上拉模式 void GPIOX_InitAll(void); // 设置指定GPIO的模式输入、输出、复用等 void GPIOX_SetMode(const GPIO_PinDef* pin GPIOMode_TypeDef mode); // 读取GPIO电平 uint8_t GPIOX_ReadPin(const GPIO_PinDef* pin); // 写入GPIO电平 void GPIOX_WritePin(const GPIO_PinDef* pin uint8_t level); // adc_controller.c // 初始化ADC配置多通道扫描 void ADCX_Init(void); // 读取指定通道的ADC值12位 uint16_t ADCX_ReadChannel(uint8_t channel); // pwm_controller.c // 初始化定时器产生PWM void PWMX_Init(TIM_TypeDef* TIMx uint32_t channel); // 设置PWM占空比0-1000 对应 0%-100% void PWMX_SetDuty(TIM_TypeDef* TIMx uint32_t channel uint16_t duty);4.3 固件调试技巧串口打印与LED指示在开发过程中调试信息至关重要。除了通过SWD进行单步调试一个简单的串口打印功能能解决大部分问题。// debug_uart.c // 重定向printf到串口这里用USART2作为调试口 int _write(int file char *ptr int len) { for (int i 0; i len; i) { while (!(USART2-SR USART_SR_TXE)); // 等待发送缓冲区空 USART2-DR (ptr[i] 0xFF); } return len; } // 在main函数或协议解析函数中打印信息 printf([INFO] Received command: 0x%02X\r\n cmd); printf([ERROR] Checksum mismatch! Calc:0x%02X Recv:0x%02X\r\n calc_sum recv_sum);同时在板上设计一个用户LED连接在某个GPIO上如PC13用不同的闪烁模式来表示固件的不同状态如启动成功、等待连接、接收数据、发生错误等这是一种非常直观的调试手段。5. 主机端驱动与库开发Python示例在RDK-S100的Linux系统上我们需要一个用户空间的程序或库来与扩展板通信。这里以最常用的Python为例因为它开发快捷生态丰富。5.1 串口通信基础首先需要确定扩展板连接到了RDK-S100的哪个串口设备上。通常通过USB转串口芯片连接的设备会出现在/dev/ttyUSB0或/dev/ttyACM0。我们可以通过ls /dev/tty*命令来查看插入扩展板前后设备的变化。import serial import time class PortExpansionBoard: def __init__(self port/dev/ttyUSB0 baudrate115200 timeout1): self.ser serial.Serial(port baudrate timeouttimeout) # 清空缓冲区 self.ser.reset_input_buffer() self.ser.reset_output_buffer() time.sleep(0.1) # 等待MCU启动 print(fConnected to expansion board on {port}) def _send_packet(self cmd databytes()): 构建并发送数据包 packet bytearray() packet.append(0xAA) # 帧头 packet.append(cmd) # 命令字 packet.append(len(data)) # 数据长度 packet.extend(data) # 数据域 # 计算校验和 checksum sum(packet) 0xFF packet.append(checksum) # 发送 self.ser.write(packet)5.2 实现核心控制函数基于协议我们可以封装出易用的函数。# 在 PortExpansionBoard 类中继续添加方法 def gpio_write(self pin_id level): 写GPIO电平 Args: pin_id (int): 引脚ID根据MCU固件的映射表例如1代表PA1 level (int): 0低电平 1高电平 data bytes([pin_id level]) self._send_packet(0x02 data) # 0x02是写GPIO命令 # 可以在这里接收并解析MCU的响应确认操作成功 def gpio_read(self pin_id): 读GPIO电平 data bytes([pin_id]) self._send_packet(0x01 data) # 0x01是读GPIO命令 # 接收响应包解析出电平值并返回 response self._read_response() if response and len(response) 3: return response[3] # 假设数据域的第一个字节是电平值 return None def adc_read(self channel): 读取ADC通道的值0-4095 data bytes([channel]) self._send_packet(0x03 data) # 0x03是读ADC命令 response self._read_response() if response and len(response) 4: # 假设ADC值是16位用两个字节传输 value (response[3] 8) | response[4] return value return None def _read_response(self): 读取并解析一个响应数据包 # 实现类似MCU端的状态机寻找帧头0xAA然后读取后续字节 # 验证校验和返回有效的数据部分或None pass5.3 一个完整的应用示例读取温度并控制风扇假设我们连接了一个DS18B20温度传感器使用单总线协议需MCU固件支持和一个5V风扇通过三极管或MOSFET由GPIO控制。import time from expansion_board import PortExpansionBoard # 假设我们把上面的类保存为这个模块 def main(): board PortExpansionBoard(/dev/ttyUSB0 115200) FAN_PIN 5 # 假设映射到MCU的某个GPIO TEMP_SENSOR_ID 0 # 假设只有一个温度传感器 try: while True: # 1. 读取温度 # 假设命令0x04是读取DS18B20数据域是传感器ID board._send_packet(0x04 bytes([TEMP_SENSOR_ID])) resp board._read_response() if resp: # 解析温度值假设是16位整数单位0.1摄氏度 temp_raw (resp[3] 8) | resp[4] temperature temp_raw / 10.0 print(fCurrent temperature: {temperature:.1f}°C) # 2. 根据温度控制风扇 if temperature 30.0: board.gpio_write(FAN_PIN 1) # 打开风扇 print(Fan ON) else: board.gpio_write(FAN_PIN 0) # 关闭风扇 print(Fan OFF) time.sleep(2) # 每2秒采样一次 except KeyboardInterrupt: print(\nExiting...) board.gpio_write(FAN_PIN 0) # 退出前确保风扇关闭 finally: board.ser.close() if __name__ __main__: main()这个例子展示了如何将复杂的底层硬件操作抽象成几个简单的函数调用让主控上的应用开发变得非常清晰。6. 高级话题与性能优化当基本功能跑通后我们可以考虑一些更深入的优化和扩展。6.1 协议优化从轮询到事件驱动上面的例子是主控不断轮询发送命令等待响应。对于实时性要求不高的系统可以但如果需要MCU主动上报事件如按键按下、传感器超阈值就需要改进协议。我们可以定义一种“上报数据包”。当MCU端有紧急事件时可以主动打断当前的通信流程向主控发送一个特定帧头的包。主控的串口读取线程需要能够区分“命令响应包”和“事件上报包”。这要求协议设计得更健壮可能需要加入包ID、超时重传等机制。6.2 多线程与资源管理在RDK-S100的Python程序中如果同时需要控制多个扩展板或者在进行串口通信的同时还要处理网络请求、图形界面就需要用到多线程。import threading from queue import Queue class BoardManager: def __init__(self): self.board PortExpansionBoard() self.command_queue Queue() # 命令队列 self.response_queue Queue() # 响应队列 self.running True # 启动发送线程 self.send_thread threading.Thread(targetself._send_worker) self.send_thread.start() # 启动接收线程 self.recv_thread threading.Thread(targetself._recv_worker) self.recv_thread.start() def _send_worker(self): while self.running: try: cmd data callback self.command_queue.get(timeout0.1) self.board._send_packet(cmd data) # 可以将发送记录和回调函数存入一个字典等待响应匹配 except queue.Empty: continue def _recv_worker(self): while self.running: response self.board._read_response() if response: # 解析响应找到对应的发送记录调用回调函数处理结果 self.response_queue.put(response) def submit_command(self cmd data callbackNone): 提交一个异步命令 self.command_queue.put((cmd data callback)) def gpio_write_async(self pin_id level callback): self.submit_command(0x02 bytes([pin_id level]) callback)这样主线程只需要调用gpio_write_async并提供一个处理响应的回调函数即可不会阻塞。6.3 固件升级OTA考虑一个成熟的产品需要支持固件升级。我们可以为MCU固件预留一个Bootloader。Bootloader是一段存储在MCU Flash起始位置的小程序它上电后先运行检查某个条件如某个GPIO的电平或者串口是否有特定的升级命令。如果条件满足就通过串口接收新的应用程序固件数据并写入到Flash的应用程序区域。完成后跳转到应用程序执行。实现一个简单的串口Bootloader并不复杂关键在于Flash的擦写操作和程序跳转。ST官方提供了AN2606应用笔记和相关的示例代码是很好的参考。7. 实战避坑与经验分享做了这么多最后分享几个我实际调试中遇到的“坑”和解决思路希望能帮你少走弯路。坑一串口通信乱码或丢包现象主控发送命令MCU收不到或者收到的是乱码偶尔能通。排查检查波特率这是最常见的原因。确保主控和MCU的波特率、数据位、停止位、校验位完全一致。最好在代码初始化串口后用printf打印出配置参数核对。检查电平用万用表量一下MCU的TX引脚在发送时的电压是否是3.3VRDK-S100的RX引脚能否识别这个电平如果RDK-S100是1.8V电平那就必须用电平转换芯片。检查地线确保主控和扩展板之间有良好的共地。地线不通或阻抗过大是通信不稳定的元凶之一。软件流控确认是否无意中使能了RTS/CTS硬件流控。如果使能了但电路没接会导致通信卡死。通常我们禁用流控serial.Serial中rtsctsFalse。坑二GPIO控制失灵输出电平不对现象代码设置了高电平但用万用表量引脚只有1点几伏带不动负载。排查初始化模式STM32的GPIO在输出前必须正确初始化其模式推挽输出、开漏输出等、速度和上下拉。推挽输出才能提供强驱动能力。负载过重查看MCU GPIO的驱动能力通常±20mA。如果你直接驱动一个继电器线圈可能需要上百mA肯定会拉低电压。必须使用三极管或MOSFET来驱动。引脚冲突检查该GPIO是否被复用于其他功能如串口、SPI。在初始化你的功能前可能需要先禁用其他外设对该引脚的控制。坑三ADC采样值跳动大现象读取的ADC值不稳定即使输入电压固定数值也在上下跳动。排查电源噪声这是首要怀疑对象。用示波器看MCU的VDDA模拟电源引脚纹波是否过大确保模拟电源和数字电源之间用磁珠或0欧电阻隔离并搭配足够的滤波电容。参考电压STM32F103的ADC参考电压默认是VDDA。如果VDDA不稳ADC结果自然不稳。可以尝试使用外部精密基准电压源如REF3030连接到VREF引脚。采样周期适当增加ADC的采样周期ADC_SampleTime让采样电容有更充分的时间充电到被测电压可以提高精度尤其对于高阻抗信号源。软件滤波在软件端对连续采样的多个值进行平均或中值滤波能有效平滑结果。坑四同时操作多个外设时系统卡死现象当同时进行串口通信、PWM输出、ADC采样时程序偶尔会跑飞或卡住。排查中断冲突检查不同外设的中断优先级NVIC是否设置合理。高优先级的中断如果处理时间过长会阻塞低优先级中断甚至导致看门狗超时复位。给关键任务如通信分配较高优先级给耗时任务如ADC DMA传输完成分配较低优先级。堆栈溢出如果中断服务函数里调用了大量函数或使用了大型局部数组可能导致堆栈溢出。可以尝试增大启动文件startup_stm32f103xe.s中定义的堆栈大小。使用DMA对于像ADC多通道扫描、串口大量数据收发这类任务强烈建议使用DMA直接存储器访问。它可以在不占用CPU的情况下搬运数据极大提高系统效率和实时性。设计这样一个扩展板从硬件选型、画原理图PCB到固件开发、驱动编写最后联调测试是一个完整的嵌入式产品开发流程。它不仅仅是一个简单的“转接板”而是一个需要软硬件深度结合的系统。当你看到通过自己编写的几行Python代码就能灵活控制远端的几十个硬件引脚时那种成就感是非常直接的。这个项目最大的价值在于它为你基于RDK-S100这类高性能AIoT主控进行复杂硬件创新提供了一个可靠、可扩展的底层支撑让你能更专注于上层应用逻辑的实现。