ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32F429 UART7寄存器版串口初始化与收发详解

STM32F429 UART7寄存器版串口初始化与收发详解 简介面向STM32F429嵌入式开发者的串口7寄存器版示例程序专门演示该串口外设的底层寄存器配置流程适合需要脱离库函数、直接操作寄存器完成串口通信的工程师或学习者。RAR压缩包共55个文件体积仅756KB涵盖C/H源文件、Keil工程文件uvprojx/uvoptx、编译中间产物o/d/crf/lst以及可下载的hex/axf文件并包含分模块的HARDWARE、SYSTEM、USER目录结构便于定位和移植。程序代码完整展示了串口7从时钟使能到中断收发的全过程配置RCC_APB1ENR开启串口时钟设置USART_CR1/CR2/CR3的数据位、停止位、校验、硬件流控通过BRR计算波特率再借助SR与DR实现发送接收及错误标志处理对理解寄存器底层机制非常有帮助。整个示例在MDK中打开即可编译运行适合裸机开发、竞赛备赛或项目前期验证。目前已有530人学习下载是一份轻量但不失完整的STM32F429串口7寄存器编程参考也可作为二次开发的起点。 在项目里接手了一个基于STM32F429的板子外设接口已经由硬件工程师定死了其中一路调试串口指定用串口7UART7而且软件框架里明确要求“寄存器版”不能用HAL库也不建议用标准外设库。当时第一反应是“用寄存器写串口这在F1上不是老熟脸吗”但实际动手之后发现F429的UART7有几个跟F1明显不同的地方尤其是时钟源、引脚复用AF映射和波特率分频的计算稍不注意就会踩坑。这篇文章就围绕“STM32F429串口7寄存器版程序”这个主题把从时钟到引脚、从初始化到中断收发的完整过程拆开讲清楚。这篇文章适合谁看主要是两类人一类是需要在F429上实现裸机串口程序、但又不想把整个HAL库拖进来的开发者另一类是刚开始学寄存器操作想搞明白串口底层到底是怎么工作的同学。内容不涉及RTOS也不讨论DMA就是把寄存器版的UART7从零到能收发这件事讲透代码可以直接抄但更重要的是理解每一步在干什么、为什么这么干。1. 串口7在F429上的资源定位与时钟树关系先解决一个最基础的问题为什么是串口7而不是USART1或者USART6STM32F429除了常见的USART1~USART6还有UART7和UART8。USART和UART的区别在于USART支持同步模式有SCLK时钟输出而UART只有异步模式。UART7和UART8就是纯异步串口没有SCLK引脚但功能上做普通调试口、数据透传完全够用。实际项目中经常剩下的高速串口被别的功能占了UART7作为补充通道很常见。从时钟树上看UART7挂在APB1总线上。F429的APB1时钟最高是45MHz注意这个45MHz很重要因为波特率计算要用它。很多人习惯性以为串口都挂APB2那是因为USART1和USART6确实挂在APB2最高90MHz但UART7不是它跟USART2、USART3、UART4、UART5一样都在APB1上。如果拿着USART1的时钟90MHz去算UART7的波特率出来的波特率误差会直接到错乱级别这是最经典的坑之一。再来看引脚。UART7在F429上有两组常用的引脚映射一组是PE8TX/PE7RX另一组是PF7TX/PF6RX还有一组是PH14TX/PH13RX。具体用哪组要看板子的原理图不能凭习惯选。我用的板子把UART7接到了PE8和PE7所以后面的代码就基于这两个引脚来写。这里要特别提醒GPIO的复用功能编号AF不是随便填的F429的UART7对应AF7这个值写错的话引脚就完全不出波形。还有一个容易被忽略的地方是GPIO时钟。使用PE7/PE8的话需要使能GPIOE的时钟如果选PF6/PF7就要开GPIOF。同时UART7本身也有自己的时钟使能位在RCC_APB1ENR寄存器的bit5UART7EN。这个位的偏移位置在不同型号上不同F4系列跟F1系列不一样不能照搬F1的代码。2. 寄存器版初始化过程解读核心代码2.1 使能时钟第一步永远是时钟。GPIOE和UART7都挂在各自的RCC寄存器上GPIOE的时钟使能在RCC_AHB1ENR的第4位GPIOEENUART7的时钟使能在RCC_APB1ENR的第5位UART7EN。代码如下#define RCC_AHB1ENR (*(volatile unsigned int *)0x40023830) #define RCC_APB1ENR (*(volatile unsigned int *)0x40023840) RCC_AHB1ENR | (1 4); // GPIOE 时钟使能 RCC_APB1ENR | (1 5); // UART7 时钟使能为什么GPIOE是AHB1ENR的第4位而UART7是APB1ENR的第5位这就要回到F429的总线架构GPIO挂在AHB1上串口挂在APB1上。AHB1ENR和APB1ENR是两套不同的寄存器分别控制不同总线上的外设。这个区分在F1时代不太明显因为F1的GPIO挂在APB2上很多人习惯了“所有外设都能在一个RCC寄存器里找”到F4就会乱。记住一句话具体的位号以参考手册的RCC寄存器位定义表为准不要靠背。这里有个小经验开时钟的语句写完最好顺手读一下对应的寄存器确认bit真的置上了再往下走。虽然正常不会失败但在调试器下单步观察一次对理解总线时钟树和排错都有帮助。2.2 GPIO复用配置GPIO引脚要工作在复用功能模式并且选择正确的AF编号。PE7、PE8要设为复用模式MODER寄存器对应位为10速度设为高速OSPEEDR为11同时AFR寄存器选择AF7。代码如下#define GPIOE_MODER (*(volatile unsigned int *)0x40021000) #define GPIOE_OTYPER (*(volatile unsigned int *)0x40021004) #define GPIOE_OSPEEDR (*(volatile unsigned int *)0x40021008) #define GPIOE_AFRL (*(volatile unsigned int *)0x40021020) #define GPIOE_AFRH (*(volatile unsigned int *)0x40021024) // PE7 - AF7 (RX) // PE8 - AF7 (TX) GPIOE_MODER ~((0x3UL 14) | (0x3UL 16)); // 先清空对应 PE7 / PE8 GPIOE_MODER | ((0x2UL 14) | (0x2UL 16)); // 设置为复用功能 GPIOE_OSPEEDR | ((0x3UL 14) | (0x3UL 16)); // 高速 GPIOE_AFRL ~(0xFUL 28); // PE7 在低寄存器 AFR7位段 GPIOE_AFRL | (0x7UL 28); // 选择 AF7 GPIOE_AFRH ~(0xFUL 0); // PE8 在高寄存器 AFR8位段 GPIOE_AFRH | (0x7UL 0); // 选择 AF7注意PE7和PE8的AF编号都填7但PE7落在GPIOE_AFRL控制PE0~PE7PE8落在GPIOE_AFRH控制PE8~PE15。这是个很容易错的地方只改了一个寄存器另一个引脚的AF没设置就会出现“TX有波形、RX收不到”或反过来。两个引脚必须都配置为AF7否则串口只能单工工作。还有一个小细节复用功能的推挽/开漏选择。串口发送引脚通常用推挽输出接收引脚由外设控制不需要额外设推挽。但有一个场景需要注意如果板子上外部接了上拉电阻或者对方设备是开漏输出可能需要把RX引脚配置为上拉输入。寄存器版里如何设置其实复用模式下GPIO的上下拉由PUPDR寄存器控制。串口复用功能下RX的上下拉到底是硬件自动搞定还是需要软件配置不同芯片行为不太一样。F429的实践结果是默认不配置PUPDR也能正常工作但在噪声比较大的环境里建议把RX配上拉会更稳定。2.3 UART7寄存器初始化与波特率计算UART7的基地址是0x40007800。串口寄存器初始化主要涉及CR1、CR2、BRR三个寄存器。CR1控制串口使能、发送使能、接收使能、中断使能等CR2控制停止位BRR是波特率分频寄存器。直接看代码#define UART7_BASE (0x40007800UL) #define UART7_CR1 (*(volatile unsigned int *)(UART7_BASE 0x0C)) #define UART7_CR2 (*(volatile unsigned int *)(UART7_BASE 0x10)) #define UART7_BRR (*(volatile unsigned int *)(UART7_BASE 0x08)) // 计算 BRR // APB1 45MHz, Baud 115200 // DIV 45000000 / (16 * 115200) 24.414 // 整数部分 24, 小数部分 0.414 * 16 6.6 ≈ 7 // BRR (24 4) | 7 391 UART7_BRR 391; UART7_CR2 ~(0x3UL 12); // 清除停止位 UART7_CR2 | (0x0UL 12); // 1位停止位 UART7_CR1 (1 13) | (1 3) | (1 2); // UE1, TE1, RE1这里重点是波特率计算。F429的串口波特率公式是Baud PCLK / (16 * USARTDIV)其中USARTDIV是一个带小数的分频值。BRR寄存器的低4位是小数部分高16位是整数部分。具体到我用的45MHz APB1和115200波特率45MHz / (16 * 115200) 24.414。整数部分24放进BRR的高位小数部分0.414乘以16等于6.6四舍五入取7放进低4位所以BRR (24 4) | 7 391。如果要精确到小数位C语言里可以直接用浮点计算然后四舍五入uint16_t usart_div (uint16_t)((45.0f * 1000000.0f) / (16.0f * 115200.0f) 0.5f); UART7_BRR usart_div;注意我的计算里隐去了一个细节OVER8位。F4系列支持8倍过采样和16倍过采样。默认复位值OVER80即16倍过采样上面公式就是基于16倍过采样计算的。如果哪天你开了8倍过采样OVER81公式分母就从16变成8BRR的小数位含义也不一样了。所以初始化CR1的时候千万不要动OVER8位保持默认0。CR1初始化的那一行(1 13)是UE使能串口(1 3)是TE使能发送(1 2)是RE使能接收。这个顺序有个讲究UE必须是最后一个置位的。也就是说先配置BRR和CR2再配置CR1的其他功能位最后置UE。如果先开了UE再改BRR虽然大多数情况下也能工作但在部分芯片版本上会出现波特率不稳定甚至乱码的问题。工程上求稳顺序遵守手册建议。3. 收发逻辑的实现与中断处理3.1 轮询发送寄存器版的发送核心就是等TXE标志位发送数据寄存器空置1然后写DR寄存器。F429的UART7状态寄存器是SR地址偏移0x00。TXE是SR的第7位bit7。发送一个字节的代码如下#define UART7_SR (*(volatile unsigned int *)(UART7_BASE 0x00)) #define UART7_DR (*(volatile unsigned int *)(UART7_BASE 0x04)) void uart7_send_char(char ch) { while (!(UART7_SR (1 7))); // 等待 TXE UART7_DR (unsigned int)ch; }如果发送多个字节很多人会写一个循环但要注意连续发送时不仅要等TXE还要在最后一字节发送完成后等TC发送完成标志。TC是SR的第6位。特别是发送完最后一字节后如果马上进入低功耗模式或者关闭串口TC没等到会导致最后一位数据没发出去就被掐断。所以实际工程里我习惯在发送函数的末尾加一个等TC的操作或者在批量发送结束时统一等一次TC。发送字符串是串口调试最常用的函数代码很简单void uart7_send_string(char *str) { while (*str) { uart7_send_char(*str); } while (!(UART7_SR (1 6))); // 等待 TC }3.2 轮询接收与中断接收接收更简单轮询模式下等RXNE标志SR的第5位置1然后读DR寄存器。F429的DR寄存器是9位有效数据但低8位就是我们平时用的数据直接取(unsigned char)转换即可char uart7_get_char(void) { while (!(UART7_SR (1 5))); // 等待 RXNE return (char)(UART7_DR 0xFF); }但实际项目里串口很少用轮询接收因为主循环一旦忙于别的事情数据就可能被覆盖。更常用的方案是中断接收。F429的UART7中断使能在CR1的第5位RXNEIE置1后每当RXNE标志置位收到一个字节就会触发UART7全局中断。UART7的中断号在NVIC里是39。NVIC配置用寄存器操作#define NVIC_ISER1 (*(volatile unsigned int *)0xE000E104) // 使能 UART7 中断中断号 39所以对应 ISER1 的第 (39-32)7 位 NVIC_ISER1 (1 7);UART7全局中断使能之后还要在CR1里开RXNEIEUART7_CR1 | (1 5); // RXNEIE对应的中断服务函数在startup文件里命名为UART7_IRQHandler。函数里先判断是不是RXNE触发如果是就读取DR寄存器把数据存到缓冲区volatile unsigned char uart7_rx_buf[256]; volatile unsigned int uart7_rx_cnt 0; void UART7_IRQHandler(void) { if (UART7_SR (1 5)) { uart7_rx_buf[uart7_rx_cnt] (unsigned char)(UART7_DR 0xFF); if (uart7_rx_cnt 256) { uart7_rx_cnt 0; } } }实际使用中这种最简单的环形缓冲方案在“高速接收低速处理”的场景下容易丢数据尤其是当主循环没来得及消费缓冲区被写满时。更稳的做法是实现一个带读写索引的环形队列。我这里为了展示寄存器版的核心思路先用简单数组做缓冲但生产项目建议直接上环形队列原理不复杂维护head和tail两个索引head是写入位置tail是读出位置满判断和空判断各占一个状态。还有一个容易踩的坑中断标志位怎么清除。串口的RXNE标志是读DR寄存器后自动清除的不需要手动清零。但如果是溢出错误ORESR的第3位情况就不一样了。ORE标志置位会阻塞后续接收处理方式必须是“先读SR再读DR”也就是if (UART7_SR (1 3)) { unsigned int tmp UART7_SR; tmp UART7_DR; }这个操作的原理是ORE标志的清除条件是“读SR后读DR”。如果不做这个操作只要串口数据量大或者主循环忙ORE一置位接收就永久卡死。这个坑在调试高波特率通信时特别常见我最早在UART4上就吃过这个亏当时数据收发一会儿就停排查了半天才发现是ORE标志没处理。4. 实际调试中遇到的高频问题与排查技巧4.1 引脚复用配置错误导致无输出症状程序跑起来后用示波器或逻辑分析仪量PE8引脚完全没波形。原因几乎都是AF配置不对或者GPIO时钟没开对。排查思路先查原理图确认引脚编号再查对应的AHB1ENR位有没有置1最后确认AFR寄存器里是不是AF7。这里有个不太友好但很实用的技巧如果手边没有调试器可以直接把RX和TX短接然后回环测试。如果回环能收到数据说明收发链路没问题问题大概率在对方设备或接线如果回环都收不到那就是引脚或串口配置的问题。4.2 波特率误差导致的乱码症状用115200波特率发送“Hello”收到“Hllo”或者完全乱码而且越长的数据乱得越厉害。这种情况要优先检查APB1时钟是不是真的45MHz。有些代码在系统时钟初始化时把APB1分频配错了比如配成了2分频45MHz变22.5MHz或者4分频11.25MHz而HAL库或者标准库在SystemInit里会自动配好但寄存器版全裸写的话时钟系统初始化也得自己来。计算波特率时用的时钟值和实际APB1时钟值不一致必然乱码。建议在上电后把RCC_CFGR的PPRE1位段读出来确认分频系数换算成实际频率再做波特率计算。4.3 中断接收偶发丢字节症状中断能进来但偶发丢数据。可能原因有几个优先级不够或者主循环关闭了中断。F429的UART7中断优先级默认是0最高优先级组配置下可能被其他中断抢占。如果系统里还有其他高优先级中断串口中断可能会被延迟而串口接收没有硬件FIFOF4的UART没有FIFO超时了就必然丢数据。解决思路要么把UART7中断优先级设高要么用DMA接收来缓解CPU延迟问题。寄存器版配置中断优先级需要操作NVIC_IPR寄存器UART7对应IPR的第39位分配在两个字节里。这个优先级配置虽然麻烦但值得花时间调对因为接收丢数据的问题往往不是配置错误而是优先级策略不对。4.4 串口长时间运行后卡死这是最隐蔽的问题。症状程序运行十几分钟甚至几小时后串口无响应但看门狗也没复位。绝大多数情况就是ORE溢出错误没有处理。串口不断收到数据而主循环很少读缓冲区满了之后接下来每个字节都触发ORE。如果中断函数里没做“读SR再读DR”的清除操作后续接收就被锁死。在干扰比较强的工业环境即使波特率不高也可能因为一个毛刺导致溢出错位连续几个错误帧之后ORE就永久置位了。所以上电初始化后第一件事就是清一次ORE标志然后在整个中断处理函数里对所有错误标志ORE、NE、FE、PE统一做清除处理。我这里实际用的初始化版本里中断处理函数做了一件事无条件先读SR再读DR确保错误标志清零后再判断RXNE是否置位。这个“无条件读清”的思路能覆盖大多数异常情况代码上也简洁不容易漏。4.5 常见问题速查表症状可能原因排查与解决发送无波形GPIO AF配错或引脚时钟没开检查AFR寄存器是否为AF7确认AHB1ENR对应位波特率乱码APB1时钟频率与计算值不符读CFGR的PPRE1位确认实际PCLK1只能发不能收RX引脚AF没配置或RE没置位检查AFRL/AFRH确认CR1的RE1收几个字节就停ORE溢出标志没清除读SR再读DR清标志偶发丢字节中断优先级过低或缓冲区溢出调整NVIC优先级实现环形队列最后一字节没发完没有等待TC发送末尾等TC标志冷启动第一字节乱码上电瞬间引脚电平不稳初始化后延时再发数据5. 寄存器版串口程序的几个实用增补5.1 重定向printf到串口7很多调试场景中用printf打印信息比逐字节发送方便得多。重定向的关键是让C库的fputc函数最终调用UART7发送函数。在ARMCCKeil环境下重写fputc即可int fputc(int ch, FILE *f) { uart7_send_char((char)ch); return ch; }同时要确保Keil的Options里勾选了“Use MicroLIB”否则fputc的重定向可能被半主机semihosting功能拦截导致程序硬fault。这个坑在F1时代就常见F4也一样。用GCC工具链的话需要重写_write函数逻辑类似。5.2 串口7与低功耗模式的配合如果项目里要用到WFI或者STOP模式UART7不能随便关掉时钟。在STOP模式下APB1时钟会停UART7无法工作。但可以用一个技巧进入STOP前把RX引脚配置为外部中断输入靠串口空闲时的起始位跳变唤醒MCU。这个思路实际上是把串口RX当作边沿触发的外部中断源来用。实现方式是把GPIO的AF模式临时换成输入模式并开启EXTI中断。唤醒后再重新配置回AF模式UART7重新初始化。这个切换过程要快否则对方设备发的数据可能已经超时了。我在低功耗项目里实测115200波特率下从唤醒到重新初始化UART7大约花费几十微秒对方设备只要不连续发送两个字节间隔小于这个时间就不会丢数据。5.3 寄存器版的优势与适用场景讨论寄存器版和HAL库哪个好本身没有标准答案。但从寄存器版入手至少有三个明显的好处一是中断响应时间可控没有HAL库那层回调函数的额外开销二是代码体积小在Flash资源紧张时优势明显三是对底层工作原理的理解更透彻方便排查硬件问题。缺点也很明显开发速度慢、可移植性差、代码可读性取决于个人风格。我个人的习惯是原型验证阶段用HAL库赶速度到产品化阶段如果资源紧张或者对中断时延有要求再改成寄存器版。两个方案不冲突关键是知道自己手里的项目更适合哪种。6. 写在最后串口寄存器配置的通用套路其实串口寄存器版的套路是相通的。不管是USART1还是UART7不管是F1还是F4初始化步骤都是开GPIO时钟、开串口时钟、配置GPIO复用模式、配置串口控制寄存器、计算BRR、使能串口。区别只在于具体寄存器地址和位号不同。只要把基地址查对、把位号查对、把总线时钟查对剩下的代码逻辑几乎可以照搬。所以我建议第一次接触寄存器版串口的同学不要急着抄代码先打开芯片参考手册Reference Manual把RCC、GPIO、UART这三个章节的寄存器位定义过一遍花半小时把时钟树看明白后面写起来会非常有底气。另一个建议是善用调试器。寄存器版程序最怕的就是寄存器写错位而调试器里可以直接看寄存器实时值。执行完初始化后把UART7_CR1、UART7_CR2、UART7_BRR、GPIOE_AFRL、GPIOE_AFRH这几个关键寄存器值过一遍确认没有意外的位被置上基本就能排除配置问题。我在调试中经常这么干比对着代码一行行猜快得多。本文还有配套的精品资源点击获取
返回列表