在嵌入式开发中,STM8S003F3P6因其性价比高、外设丰富而广受欢迎。但这款MCU仅有一个硬件UART接口,当项目需要同时连接多个串口设备时,开发者常陷入资源不足的困境。本文将带你深入探索一种创新解决方案——通过普通GPIO口软件模拟UART通信,实现真正的"无中生有"。
硬件串口虽然方便,但在资源受限的MCU上往往成为稀缺资源。STM8S003F3P6的典型应用场景包括:
硬件UART与软件模拟UART的关键对比:
| 特性 | 硬件UART | 软件模拟UART |
|---|---|---|
| 占用资源 | 专用硬件模块 | 普通GPIO+定时器 |
| 最大波特率 | 通常较高(≥115200) | 受CPU限制(通常≤9600) |
| 开发复杂度 | 低 | 中高 |
| 灵活性 | 固定引脚 | 任意GPIO |
| 中断负载 | 轻 | 较重 |
提示:模拟UART最适合用于低速、间歇性数据传输场景,如配置参数下发、状态报告等。
UART通信的本质是按照特定时序收发高低电平。一个完整的UART帧包括:
实现模拟UART的三要素:
以下是基本的位时序计算公式:
code复制位时间(μs) = 1,000,000 / 波特率
例如9600波特率时,每位持续104μs。实际代码中需要根据系统时钟精确计算定时器重载值。
选择两个普通GPIO引脚分别作为TX和RX,例如:
连接方式与硬件UART相同,注意:
模拟UART的实现通常有两种方式:
我们推荐使用定时器中断+状态机的混合方案:
c复制typedef enum {
UART_IDLE,
UART_START_BIT,
UART_DATA_BITS,
UART_STOP_BIT
} uart_state_t;
volatile uart_state_t rx_state = UART_IDLE;
volatile uint8_t rx_byte = 0;
volatile uint8_t rx_bit_count = 0;
发送相对简单,只需按照时序切换GPIO状态:
c复制void soft_uart_send_byte(uint8_t data) {
// 起始位
GPIO_WriteLow(SOFT_UART_TX_PORT, SOFT_UART_TX_PIN);
delay_us(BIT_TIME);
// 数据位(LSB first)
for(uint8_t i = 0; i < 8; i++) {
if(data & 0x01) {
GPIO_WriteHigh(SOFT_UART_TX_PORT, SOFT_UART_TX_PIN);
} else {
GPIO_WriteLow(SOFT_UART_TX_PORT, SOFT_UART_TX_PIN);
}
data >>= 1;
delay_us(BIT_TIME);
}
// 停止位
GPIO_WriteHigh(SOFT_UART_TX_PORT, SOFT_UART_TX_PIN);
delay_us(BIT_TIME);
}
接收需要更精确的时序控制,建议使用定时器中断:
c复制INTERRUPT_HANDLER(TIM4_UPD_OVF_IRQHandler, 23) {
static uint8_t sample_count = 0;
switch(rx_state) {
case UART_IDLE:
if(GPIO_ReadInputPin(SOFT_UART_RX_PORT, SOFT_UART_RX_PIN) == RESET) {
rx_state = UART_START_BIT;
sample_count = 0;
}
break;
case UART_START_BIT:
if(++sample_count == 3) { // 在起始位中间采样
if(GPIO_ReadInputPin(SOFT_UART_RX_PORT, SOFT_UART_RX_PIN) == RESET) {
rx_state = UART_DATA_BITS;
rx_bit_count = 0;
rx_byte = 0;
sample_count = 0;
} else {
rx_state = UART_IDLE; // 起始位验证失败
}
}
break;
case UART_DATA_BITS:
if(++sample_count == 3) {
if(GPIO_ReadInputPin(SOFT_UART_RX_PORT, SOFT_UART_RX_PIN)) {
rx_byte |= (1 << rx_bit_count);
}
if(++rx_bit_count >= 8) {
rx_state = UART_STOP_BIT;
}
sample_count = 0;
}
break;
case UART_STOP_BIT:
if(++sample_count == 3) {
rx_state = UART_IDLE;
// 将rx_byte存入接收缓冲区
}
break;
}
TIM4_ClearITPendingBit(TIM4_IT_UPDATE);
}
STM8S003F3P6的16MHz内部时钟经过分频后,可能无法精确产生某些波特率。例如:
解决方案:
模拟UART更容易受到干扰,建议:
为减轻CPU负担,可以:
c复制// 示例:动态开启/关闭接收
void soft_uart_enable_rx(void) {
rx_state = UART_IDLE;
TIM4_ITConfig(TIM4_IT_UPDATE, ENABLE);
}
void soft_uart_disable_rx(void) {
TIM4_ITConfig(TIM4_IT_UPDATE, DISABLE);
}
在最近的一个温湿度监测项目中,我们需要STM8S003F3P6同时连接:
模拟UART设置为4800bps,工作稳定。关键配置参数:
c复制#define SOFT_UART_BAUDRATE 4800
#define BIT_TIME (1000000/SOFT_UART_BAUDRATE)
#define SAMPLE_POINT (BIT_TIME/2)
遇到的坑与解决方案:
问题:高优先级中断导致位定时不准
解决:调整模拟UART中断优先级为中等
问题:长线传输时信号质量差
解决:在RX引脚添加1nF电容滤波
问题:连续发送时丢失数据
解决:在发送函数中添加忙等待检测