1. 软件串口的基本原理
什么是串口通信?
- 串口(UART):异步串行通信协议,数据以固定格式传输:
- 帧格式:1 个起始位(0)、8 个数据位、1 个结束位(1),共 10 位。
- 波特率:每秒传输的位数,例如 115200bps 表示每秒 115200 位。
- 位时长:1 / 波特率,例如 115200bps 的位时长 ≈ 8.68µs。
硬件 UART vs 软件串口
- 硬件 UART:
- 使用 STM32 的内置 USART 外设,波特率由时钟分频器控制。
- 优点:高效、可靠。
- 缺点:波特率固定,无法动态调整单个位的时长。
- 软件串口:
- 用定时器控制时间,用 GPIO 输出/读取电平。
- 优点:灵活,可实现波特率拉偏。
- 缺点:CPU 占用高,依赖定时器精度。
软件串口的关键
- 发送:定时器触发中断,逐位输出到 GPIO。
- 接收:外部中断检测起始位,定时器采样数据位。
2. 波特率与时钟的关系
波特率计算
- 波特率 = 时钟频率 / (每位所需的计数值)。
- 位时长 = 1 / 波特率。
- 示例:
- 目标波特率 = 115200bps。
- 位时长 = 1 / 115200 ≈ 8.68µs。
时钟配置
假设使用 STM32F103:
- 系统时钟:72MHz(典型配置)。
- 定时器时钟:
- 通过预分频器(Prescaler)调整,例如设为 72,则定时器时钟 = 72MHz / 72 = 1MHz(周期 1µs)。
- 定时器计数:
- 每个计数 = 1 / 1MHz = 1µs。
- 115200bps 的位时长 8.68µs ≈ 8.68 个计数。
- 设置定时器重装值(ARR)为 8(计数 0 到 8,共 9 次),周期 ≈ 9µs,波特率 ≈ 111111bps(接近 115200bps)。
调整精度
- 如果需要更精确:
- 降低预分频器,例如设为 36,则定时器时钟 = 2MHz(周期 0.5µs)。
- 位时长 8.68µs ≈ 17.36 个计数,ARR = 16(计数 17 次),周期 8.5µs,波特率 ≈ 117647bps。
3. 软件串口代码示例
硬件假设
- MCU:STM32F103。
- TX 引脚:GPIOA Pin 0。
- RX 引脚:GPIOA Pin 1。
- 定时器:TIM2(发送),TIM3(接收)。
- 目标波特率:115200bps
发送代码
#include "stm32f1xx_hal.h"
TIM_HandleTypeDef htim2, htim3;
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 全局变量
uint8_t tx_buffer[] = {0xAA}; // 发送数据:0xAA (10101010)
uint8_t tx_bit_index = 0;
uint8_t rx_buffer = 0;
uint8_t rx_bit_index = 0;
void SystemClock_Config(void); // 默认 72MHz 配置
// 初始化 GPIO 和定时器
void SoftwareUART_Init(void) {
// GPIO 配置
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_0; // TX
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_1; // RX
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 外部中断检测起始位
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// TIM2 配置 (发送)
__HAL_RCC_TIM2_CLK_ENABLE();
htim2.Instance = TIM2;
htim2.Init.Prescaler = 71; // 72MHz / 72 = 1MHz
htim2.Init.Period = 8; // 9µs ≈ 111111bps
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_Base_Init(&htim2);
HAL_TIM_Base_Start_IT(&htim2); // 中断模式
// TIM3 配置 (接收)
__HAL_RCC_TIM3_CLK_ENABLE();
htim3.Instance = TIM3;
htim3.Init.Prescaler = 71;
htim3.Init.Period = 4; // 半个位时长 4.5µs
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_Base_Init(&htim3);
// NVIC 配置
HAL_NVIC_SetPriority(EXTI1_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(EXTI1_IRQn);
HAL_NVIC_SetPriority(TIM2_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(TIM2_IRQn);
HAL_NVIC_SetPriority(TIM3_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(TIM3_IRQn);
}
接受代码
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if (GPIO_Pin == GPIO_PIN_1) { // 起始位下降沿
rx_buffer = 0;
rx_bit_index = 0;
HAL_TIM_Base_Start_IT(&htim3); // 启动采样
}
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM3) { // 接收定时器
if (rx_bit_index < 9) { // 采样 9 次(起始 + 8 数据)
if (rx_bit_index > 0) { // 跳过起始位
uint8_t bit = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1);
rx_buffer |= (bit << (rx_bit_index - 1));
}
rx_bit_index++;
} else {
HAL_TIM_Base_Stop(&htim3); // 接收完成
printf("Received: 0x%02X\n", rx_buffer);
}
}
}
- 流程:
- 外部中断检测起始位下降沿。
- 定时器以半个位时长(4.5µs)采样,确保落在每位中间。
- 读取 8 个数据位,重建字节。
- 波形解析:
- 输入 0xAA:
- 采样点:0 (起始), 0, 1, 0, 1, 0, 1, 0, 1。
- 结果:rx_buffer = 0xAA。
- 输入 0xAA:
4. 波特率与时钟的详细计算
发送时钟
- 系统时钟:72MHz。
- 预分频器:72 → 定时器时钟 = 1MHz(1µs)。
- ARR:8 → 周期 = 9µs。
- 波特率:1 / 9µs = 111111bps。
- 误差:(115200 - 111111) / 115200 ≈ 3.5%,可接受。
接收时钟
- 预分频器:72 → 1MHz。
- ARR:4 → 周期 = 5µs(半个位时长)。
- 采样时机:起始位后 4.5µs 开始,每 9µs 采样一次。
拉偏示例
- 拉偏 0xAA 的第 3 位:
- 标准:每位 9µs。
- 拉偏 +10%:第 3 位变为 9.9µs(ARR = 9 → 10)。
- 中断中动态调整:
5. 软件串口原理总结
- 发送:
- 定时器控制每位时长,GPIO 输出电平。
- 波特率由 ARR 和时钟频率决定。
- 接收:
- 外部中断触发,定时器采样。
- 半个位时长采样确保精度。
- 时钟与速率:
- 定时器时钟 = 系统时钟 / 预分频器。
- 位时长 = (ARR + 1) * 时钟周期。