2026/6/20 3:47:39
网站建设
项目流程
制作网站如何选择主机,公司网站建设要注意什么问题,国都建设集团网站,威海网站建设哪家的好从零实现UART发送#xff1a;一个嵌入式工程师的底层实践课你有没有过这样的经历#xff1f;代码烧进去#xff0c;串口助手打开#xff0c;满怀期待地等着“Hello World”出现——结果屏幕上全是乱码#xff0c;或者干脆一片空白。这时候#xff0c;你会不会下意识地怀疑…从零实现UART发送一个嵌入式工程师的底层实践课你有没有过这样的经历代码烧进去串口助手打开满怀期待地等着“Hello World”出现——结果屏幕上全是乱码或者干脆一片空白。这时候你会不会下意识地怀疑是不是线接反了波特率设错了甚至开始怀疑人生别急这正是我们今天要解决的问题。在STM32开发中能用库函数点亮LED只是入门而能从寄存器层面让串口吐出第一个字节才算真正踏进了嵌入式系统的大门。本文不讲HAL不谈CubeMX我们要做的是亲手把一个字节通过GPIO引脚一比特一比特地“推”出去。这不是模拟bit-banging而是利用MCU内置的UART硬件模块直接操作寄存器完成一次完整的串行数据发送。整个过程不依赖任何高级驱动目标明确让你看清楚每一个环节背后发生了什么。为什么非得“从零”写一遍UART现在大多数项目都用HAL或LL库几行MX_USART1_UART_Init()就搞定了。那为什么还要费劲去写寄存器因为——当你遇到启动阶段无法使用RTOS、内存紧张不能加载完整库、甚至Bootloader里需要最简日志输出时你会需要一段只靠几个寄存器就能打出调试信息的代码。更重要的是只有亲手配置过时钟、算过波特率、等过TXE标志位你才会真正理解“异步串行通信”到底是什么意思。这不是复古这是基本功。我们要做什么目标拆解我们的最终目标很简单上电后通过PA9引脚USART1_TX以115200波特率持续发送字符串Hello, UART!\r\n。为实现这个目标我们需要一步步完成以下任务使能相关外设时钟配置TX引脚为复用推挽输出计算并设置正确的波特率配置UART基本参数8N1编写轮询方式的字节发送函数验证输出用串口助手看到清晰可读的文字每一步我们都将直接操作STM32F1系列的寄存器拒绝封装拒绝抽象。第一步让外设“活过来”——时钟与GPIO初始化所有外设工作的前提是什么通电 有时钟。在STM32中“通电”就是使能RCCReset and Clock Control中的对应时钟。没有这一步哪怕你写了再多寄存器也是对空气操作。1.1 时钟使能先给USART和GPIO供电// 启用GPIOA和USART1时钟 RCC-APB2ENR | RCC_APB2ENR_IOPAEN; // GPIOA时钟开 RCC-APB2ENR | RCC_APB2ENR_USART1EN; // USART1时钟开这里有个细节USART1挂在APB2总线上而APB2最高支持72MHz假设系统时钟已配好。如果你用的是USART2则要操作APB1时钟寄存器RCC-APB1ENR。⚠️ 常见坑点忘了开时钟 → 寄存器写不进去现象是“配置无效”。1.2 配置PA9为复用功能推挽输出PA9是默认的USART1_TX引脚。它不是普通IO必须工作在“复用推挽输出”模式下才能由UART硬件接管控制权。STM32F1的GPIO高8位PA8~PA15由CRH寄存器控制// 清除PA9原有配置4位MODE 4位CNF GPIOA-CRH ~(0xF (9 - 8)*4); // 设置为输出速度50MHzMODE[1:0]11复用推挽输出CNF[1:0]10 GPIOA-CRH | (GPIO_CRH_MODE9_1 | GPIO_CRH_MODE9_0); // 50MHz GPIOA-CRH | (GPIO_CRH_CNF9_1); // 复用推挽位域含义MODE9[1:0] 11最大输出速度50MHzCNF9[1:0] 10复用功能推挽输出✅ 小贴士为什么不用CNF9[0]因为在复用推挽模式下CNFy[1:0] 10即可无需额外设置。至此物理通道已经准备就绪PA9可以输出UART信号了。第二步让节奏“对上”——精确生成波特率UART是异步通信没有共享时钟线。双方只能靠“约定好的速率”来采样数据。如果MCU发得快PC收得慢就会错位导致乱码。所以波特率必须足够精确。一般要求误差小于±2%。2.1 波特率怎么来的STM32的UART模块采用16倍过采样机制每个bit周期内采样16次取中间第8次作为判决值抗干扰能力强。其波特率由以下公式决定[\text{DIV} \frac{f_{\text{PCLK}}}{16 \times \text{BaudRate}}]其中- ( f_{\text{PCLK}} ) 是APB总线频率这里是APB272MHz- BaudRate 是目标波特率如115200代入数值[\text{DIV} \frac{72,000,000}{16 \times 115200} \approx 39.0625]这个值要拆成整数部分mantissa和小数部分fraction写入BRR寄存器。整数部分39 → 0x27小数部分0.0625 × 16 ≈ 1 → 0x1所以BRR 0x2712.2 写入BRR寄存器void uart_set_baudrate(USART_TypeDef* usart, uint32_t baud) { uint32_t pclk 72000000; // 应动态获取此处简化 uint32_t div (pclk 8 * baud) / (16 * baud); // 四舍五入 usart-BRR div; } // 调用 uart_set_baudrate(USART1, 115200); 技巧说明(pclk 8*baud)/(16*baud)实现了四舍五入比直接截断更准。你可以手动验证72000000 / (16 * 115200) 39.0625→ 四舍五入后为39即0x271正确。第三步启动UART发动机——配置与使能现在硬件通道有了节奏也定了接下来就是“点火”。3.1 配置UART参数8数据位1停止位无校验8N1这些参数其实都在一个寄存器里搞定USART1-CR1// 配置CR1启用发送8N1模式默认就是8N1 USART1-CR1 0; // 先清零 USART1-CR1 | USART_CR1_TE; // 使能发送 USART1-CR1 | USART_CR1_UE; // 使能USART就这么简单没错。因为STM32复位后默认就是8位数据、1位停止、无校验。我们只需要打开发送功能TE和整体使能UE即可。如果你想改其他格式比如9位数据或偶校验才需要动CR1的其他位或设置CR2。3.2 等待线路空闲准备发送虽然还没开始发数据但建议在首次发送前等待一下状态寄存器while (!(USART1-SR USART_SR_TC)); // 确保上次传输完成初次运行可省略这是个好习惯尤其在复位后立即发送时避免状态异常。第四步真正发出第一个字节终于到了激动人心的时刻。UART发送的核心逻辑非常清晰查询状态寄存器SR看是否允许写入新数据当TXETransmit Data Register Empty标志置位时表示TDR空可以写往DR寄存器写入一个字节硬件自动将其移位输出移位完成后TCTransmission Complete标志置位。我们来封装两个函数4.1 发送单个字节void usart_send_byte(USART_TypeDef* usart, uint8_t data) { // 等待TDR为空 while (!(usart-SR USART_SR_TXE)) { // NOP忙等待 } // 写入数据寄存器 usart-DR data; // 可选等待整个字符帧发送完成 // while (!(usart-SR USART_SR_TC)); } 注意DR寄存器是双用途的——写的时候是TDR发送数据寄存器读的时候是RDR接收数据寄存器。硬件会根据操作方向自动切换。4.2 发送字符串有了单字节发送字符串就容易了void usart_send_string(USART_TypeDef* usart, const char* str) { while (*str) { usart_send_byte(usart, *str); } }完整主程序演示现在把这些拼起来放进main()函数int main(void) { // 1. 初始化GPIO与时钟 RCC-APB2ENR | RCC_APB2ENR_IOPAEN | RCC_APB2ENR_USART1EN; GPIOA-CRH ~(0xF 4); // PA9 清零 GPIOA-CRH | (0xB 4); // MODE911 (50MHz), CNF910 (AF PP) // 2. 设置波特率 uart_set_baudrate(USART1, 115200); // 3. 使能USART发送 USART1-CR1 | USART_CR1_TE | USART_CR1_UE; // 4. 循环发送 while (1) { usart_send_string(USART1, Hello, UART!\r\n); for (volatile int i 0; i 1000000; i); // 简单延时 } }编译、下载、打开XCOM或PuTTY选择对应COM口设置115200、8N1你将看到Hello, UART! Hello, UART! Hello, UART! ...每一行都是你亲手“推”出去的。常见问题排查清单问题现象可能原因解决方法屏幕乱码波特率不匹配检查MCU时钟是否真是72MHzPC端设置是否一致一个字都收不到引脚配置错误或未开时钟查RCC-APB2ENR和GPIOA-CRH是否正确只发一次就停了忘记加延时或中断冲突加软件延时关闭全局中断测试字符粘连发送太快没等TXE确保每次发送前都轮询TXE标志PC收不到电平不匹配使用USB转TTL模块CH340G/CP2102不要直连USB 推荐工具逻辑分析仪抓PA9波形一眼看出波特率和帧结构是否正确。更进一步我们可以做什么你现在掌握的是一套“最小可用”的串口输出能力。在此基础上可以轻松扩展出更多实用功能✅ 重定向printf只需重写fputc函数int fputc(int ch, FILE *f) { usart_send_byte(USART1, ch); return ch; }然后就可以直接用printf(Value: %d\r\n, x);打印变量了。✅ 构建简易CLI命令行接口接收部分加上中断就能做简单的命令解析器if (received_cmd ledon) { LED_ON(); }适合远程调试设备。✅ 移植到任意MCU这套思路适用于几乎所有ARM Cortex-M系列芯片。只要知道- 对应的RCC使能位- GPIO模式配置方式- UART寄存器映射就能快速移植。写在最后每一个比特都值得被看见当我们习惯了Serial.println()这种高级封装很容易忘记底层究竟发生了什么。但正是这些看似繁琐的寄存器配置、波特率计算、标志位查询构成了嵌入式系统的根基。掌握UART发送不是为了替代HAL库而是为了在库失效时仍有能力自救。下次当你面对一块新板子没有任何调试信息输出时请记住只要还有一根TX线还有一个串口助手你就还有机会让它“说话”。而这一切始于你对BRR、CR1、SR、DR这几个寄存器的理解。现在轮到你动手试试了——能不能让你的MCU在没有库的情况下说出第一句“Hello”欢迎在评论区晒出你的实验结果我们一起debug。