利用LPC802 USART模块生成精确50%占空比PWM信号
1. 项目概述与核心挑战
在嵌入式硬件开发,尤其是电机驱动、LED调光或者简单的数字信号合成领域,生成一个稳定、精确的脉宽调制信号是家常便饭。大多数工程师的第一反应都是去用微控制器内置的通用定时器,这确实是标准做法。但就像我最近在折腾NXP的LPC802这颗小巧的Cortex-M0+芯片时发现的那样,标准做法有时候会“卡壳”。具体来说,当你需要从一个奇数分频的时钟源来产生PWM时,LPC802的通用定时器模块就无法输出一个完美的50%占空比方波了。这个限制乍一看有点反直觉,毕竟分频是硬件的基本功,但深入其架构就会发现,这与其计数器的工作模式和比较逻辑的对称性有关。当分频系数为奇数时,计数器无法找到一个整数周期点,使得高电平和低电平时间绝对相等,从而导致占空比产生微小的偏差,无法达到精确的50%。
这个需求并非纸上谈兵。比如,你的主时钟是9MHz,但受限于外部传感器或驱动芯片的时序要求,你需要一个1.8MHz的时钟信号,计算一下,9MHz / 5 = 1.8MHz,这里的分频系数“5”就是个奇数。此时,如果你固执地非要用通用定时器,要么接受一个不是50%的占空比,要么就得引入更复杂的软件补偿,这无疑增加了系统的复杂性和不确定性。而LPC802另一个能输出时钟的模块——CLKOUT,也面临着同样的奇数分频限制。这似乎把路给堵死了。但嵌入式开发的乐趣就在于,总能在数据手册的角落里找到“柳暗花明又一村”的解决方案。这次,救星是USART模块,更准确地说,是它的同步主模式时钟输出功能。这个通常被我们用来做串行通信的模块,其内部有一个高精度的波特率发生器,当配置为同步主模式时,它可以将其内部时钟直接通过一个引脚输出,而这个输出信号天生就是50%占空比的方波,完全不受分频系数奇偶性的影响。下面,我就来详细拆解如何利用LPC802的USART模块,绕开定时器的限制,生成一个稳定、精确、无需软件干预的50%占空比PWM信号。
2. 方案选型与USART时钟输出原理
为什么USART可以,而通用定时器不行?这需要从两者生成信号的根本机制上理解。通用定时器生成PWM,通常依赖于一个自动重载的计数器和一个比较寄存器。计数器从0向上计数,当计数值小于比较寄存器A的值时,输出高电平;大于等于时,输出低电平,直到计数到周期值后复位。要得到50%占空比,比较寄存器A的值必须设定为周期值的一半。这里的关键在于“周期值”和“一半”都必须是整数。当输入时钟经过一个奇数N分频后作为定时器的时钟源时,定时器每个计数周期的实际时钟周期是输入时钟的N倍。为了得到目标频率F_pwm,定时器的自动重载值(即周期)需要设置为 (F_in / N) / F_pwm。如果N是奇数,这个计算过程以及后续的“一半”计算很可能无法得到整数,而比较寄存器只能存放整数值,舍入误差就导致了占空比偏离50%。
USART的同步时钟输出则走了另一条路。在同步主模式下,USART需要为外部从设备提供一个位时钟。这个时钟(U_SCLK)由USART的波特率发生器直接产生,其公式为:U_SCLK = Fclk / (BRGVAL + 1)。其中,Fclk是USART模块的输入时钟,BRGVAL是波特率发生器寄存器(BRG)中设置的一个整数值。这个电路本质上是一个分频器,它对输入时钟Fclk进行(BRGVAL+1)分频。重要的是,这个分频器输出的波形是对称的方波。无论(BRGVAL+1)是奇数还是偶数,其输出都是50%占空比。这是因为其内部电路设计保证了输出在半个分频周期为高,另外半个周期为低。因此,我们只需要将USART配置为同步主模式,并使其持续输出这个时钟,那么该时钟引脚上的信号就是一个完美的、占空比50%的PWM波。频率则由Fclk和BRGVAL共同决定。
注意:这里存在一个关键概念区分。USART模块内部有两个“时钟”:一个是用于其内部逻辑(如移位寄存器)的“模块时钟”,另一个是输出到引脚给外部设备使用的“位时钟”(U_SCLK)。我们这里要利用的正是后者。在配置时,我们需要确保USART的“模块时钟”通路正确,但最终输出到引脚并作为PWM使用的是“位时钟”信号。
基于这个原理,我们的方案流程图就非常清晰了:
- 配置系统时钟:确定主时钟频率,并确保USART模块能获得正确的时钟源。
- 引脚功能映射:将USART的时钟输出功能,通过开关矩阵分配到某个具体的GPIO引脚上。
- 计算并设置频率:根据所需的PWM频率和主时钟频率,计算BRGVAL值,并配置USART波特率发生器。
- 模式与输出控制:将USART配置为同步主模式,并启用连续时钟输出功能。
- 启动时钟输出:通过选择USART的时钟源,激活时钟输出电路。
这个方案的另一个巨大优势是“一旦启动,无需干预”。在配置为连续输出模式后,只要系统不掉电、不复位,这个PWM信号就会一直稳定输出,完全不消耗CPU资源,实现了真正的硬件级PWM生成。
3. 硬件平台与开发环境搭建
3.1 核心硬件:LPC802微控制器与开发板
这次实验的核心是NXP LPC802微控制器。这是一颗基于Arm Cortex-M0+内核的入门级MCU,最高运行频率15MHz,具备16KB Flash和2KB SRAM。其外设包括I2C、USART、SPI、一个多速率定时器、一个通用32位定时器、12位ADC等,性价比很高。我使用的是官方的LPCXpresso802开发板,型号OM40000 Rev A。这块板子将LPC802的所有引脚通过排针引出,并集成了板载调试器,通过一根Micro-USB线即可完成供电、编程和调试,非常方便。
我们的目标信号将从PIO0_8引脚输出。选择这个引脚的原因在于,根据LPC802的用户手册,USART0的时钟输出信号可以灵活地映射到多个引脚上,PIO0_8是其中之一,并且在LPCXpresso802开发板上,这个引脚正好位于Arduino兼容接口上,方便我们用示波器探头进行测量。你需要准备以下硬件:
- LPCXpresso802开发板一块。
- 一台安装有KEIL MDK或类似IDE的个人电脑。
- 一根Micro-USB数据线(用于连接开发板和电脑)。
- 一台示波器(用于观测生成的PWM波形)。
- 两根杜邦线(用于连接开发板引脚和示波器探头)。
3.2 软件环境与SDK配置
软件开发环境我选择的是KEIL MDK v5.27,编译器用的是Arm Compiler 6。当然,你也可以使用IAR或者MCUXpresso IDE,原理相通。更重要的是NXP为LPC802提供的软件开发套件。我们需要从NXP官网下载LPCXpresso802 SDK,版本我用的是v2.4.0。这个SDK包含了所有外设的驱动库、示例代码和板级支持包,能极大简化我们的开发工作。
SDK的目录结构通常很清晰。我们需要关注的是boards\lpcxpresso802\demo_apps\这个路径,我们的工程将创建在这里。为了验证本文的方案,你可以直接创建一个新的MDK工程,或者基于某个USART示例工程进行修改。关键是要正确包含SDK中的驱动源文件(drivers目录)和头文件路径。在开始写代码之前,请务必确认你的工程已经包含了fsl_usart.c/.h,fsl_swm.c/.h,fsl_power.c/.h以及fsl_clock.c/.h这些关键驱动文件。
3.3 硬件连接与测量点确认
硬件连接非常简单:
- 用Micro-USB线将开发板的CN1接口连接到电脑。
- 找到开发板上的Arduino接口CN3。根据开发板原理图,找到标有PIO0_8的排针。这通常对应Arduino接口的某个数字引脚。
- 将示波器探头的信号线(钩子)连接到PIO0_8引脚,探头的接地线连接到开发板上的任何一个GND引脚。
实操心得:在连接示波器探头时,尽量使用探头自带的接地弹簧而不是长长的接地夹线,这样可以减少测量回路,避免引入噪声,让观察到的波形更干净。特别是当PWM频率达到MHz级别时,这个细节很重要。
4. 分步实现:从时钟源到PWM输出
下面,我将结合代码片段,详细讲解每一个配置步骤。我们的目标是生成一个1.8MHz、50%占空比的PWM信号,假设系统主时钟为9MHz。
4.1 第一步:配置系统主时钟为9MHz
LPC802的系统时钟可以来自内部的FRO。FRO可以输出9MHz, 12MHz, 15MHz等频率。我们需要将其设置为9MHz,并作为系统主时钟。
#include "fsl_power.h" #include "fsl_clock.h" void BOARD_BootClockFRO18M(void) { // 关闭看门狗时钟(可选,但推荐在初始化早期进行) CLOCK_DisableClock(kCLOCK_Wdt); // 设置FRO为系统时钟源,并配置其输出频率。 // 注意:函数名中的`18M`指的是FRO的振荡器频率,经过分频后得到9MHz的系统时钟。 POWER_DisablePD(kPDRUNCFG_PD_FRO_OUT); CLOCK_SetupFROClocking(12000000U); // 此函数内部会调用ROM API配置FRO CLOCK_SetFLASHAccessCyclesForFreq(9000000U); // 设置Flash访问等待周期以适应9MHz CLOCK_AttachClk(kFRO_to_MAIN_CLK); // 将FRO连接到主时钟 // ... 其他外设时钟的默认配置 }在main函数最开始,调用BOARD_BootClockFRO18M()即可。这个函数是SDK提供的,它封装了底层ROM API的调用,将FRO配置为18MHz振荡,然后通过内部分频给系统提供9MHz的主时钟。这一步确保了整个芯片,包括后续的USART模块,有一个稳定的9MHz时钟基础。
4.2 第二步:将USART0时钟输出映射到PIO0_8引脚
LPC802的引脚功能非常灵活,通过开关矩阵模块,可以将大部分数字外设功能映射到几乎任何I/O引脚。我们需要把USART0的时钟输出功能(kSWM_USART0_SCLK)分配给PIO0_8。
#include "fsl_swm.h" #include "fsl_iocon.h" void BOARD_InitPins(void) { // 初始化SWM(开关矩阵)和IOCON(I/O配置)模块的时钟 CLOCK_EnableClock(kCLOCK_Swm); CLOCK_EnableClock(kCLOCK_Iocon); // 将USART0的SCLK(时钟输出)功能分配到PIO0_8引脚 SWM_SetMovablePinSelect(SWM0, kSWM_USART0_SCLK, kSWM_PortPin_P0_8); // 配置PIO0_8的I/O属性:禁用上下拉电阻、标准模式、非反转 IOCON->PIO[0][8] = ((IOCON->PIO[0][8] & (~(IOCON_PIO_FUNC_MASK | IOCON_PIO_MODE_MASK | IOCON_PIO_SLEW_MASK | IOCON_PIO_INVERT_MASK | IOCON_PIO_DIGIMODE_MASK | IOCON_PIO_OD_MASK))) | IOCON_PIO_FUNC(1) // 功能选择为特殊功能(由SWM决定) | IOCON_PIO_MODE(IOCON_MODE_INACT) // 无上下拉 | IOCON_PIO_SLEW(IOCON_SLEW_STANDARD) // 标准转换速率 | IOCON_PIO_INVERT(IOCON_INVERT_DISABLE) // 不反转 | IOCON_PIO_DIGIMODE(IOCON_DIGITAL_EN) // 数字模式 | IOCON_PIO_OD(IOCON_OD_DISABLE)); // 禁用开漏输出 // 配置完成后,可以禁用SWM时钟以省电(可选) CLOCK_DisableClock(kCLOCK_Swm); }这里有几个关键点:
SWM_SetMovablePinSelect是SDK提供的函数,第一个参数是SWM实例(LPC802只有SWM0),第二个参数是要分配的外设信号,第三个参数是目标引脚。IOCON配置决定了引脚的电气特性。对于时钟输出,我们通常配置为数字输出、无上下拉、标准驱动能力。IOCON_PIO_FUNC(1)表示该引脚的功能由SWM模块决定。
4.3 第三步:计算并配置USART0输出频率
这是核心步骤。我们需要根据公式U0_SCLK = Fclk / (BRGVAL + 1)来计算BRGVAL。
- 目标频率
U0_SCLK: 1.8 MHz - USART模块输入时钟
Fclk: 在我们的配置中,USART的时钟源默认选择的是main_clk,也就是9MHz。 - 计算
BRGVAL:BRGVAL = (Fclk / U0_SCLK) - 1 = (9MHz / 1.8MHz) - 1 = 5 - 1 = 4。
因此,我们需要将波特率发生器寄存器(BRG)的值设置为4。在SDK中,这通常在初始化USART时完成。
#include "fsl_usart.h" void USART0_Configuration(void) { usart_config_t usartConfig; USART_GetDefaultConfig(&usartConfig); // 获取默认配置结构体 // 配置为同步模式、主模式 usartConfig.syncMode = kUSART_SyncMode; // 同步模式 usartConfig.slaveMode = kUSART_SlaveModeDisabled; // 主模式 // 配置波特率(本质是配置时钟输出频率) // 注意:此处的baudRate_Bps参数在同步模式下,实际配置的是BRG值对应的时钟频率 // SDK的USART_Init函数内部会根据传入的baudRate_Bps和模块时钟频率自动计算BRG值。 // 但为了精确控制,我们更推荐直接设置BRG值。 usartConfig.baudRate_Bps = 1800000U; // 传入目标频率,SDK会计算 // 初始化USART0 USART_Init(USART0, &usartConfig, 9000000U /* 传入模块时钟频率: 9MHz */); // 更直接的方法:直接操作寄存器设置BRG值 // USART0->BRG = 4; // 直接写入BRG寄存器 }注意事项:SDK的
USART_Init函数会根据你传入的baudRate_Bps和srcClock_Hz自动计算并设置BRG寄存器。在大多数情况下这很方便。但如果你需要非常精确地控制,或者想确认设置的值,可以直接读写USART0->BRG寄存器。对于1.8MHz输出和9MHz输入时钟,BRG寄存器值必须为4。
4.4 第四步:配置USART0为同步主模式并启用连续输出
仅仅设置频率还不够,必须明确告诉USART模块:“请进入同步主模式,并且持续输出时钟,不要等待数据发送。”
// 在USART_Init之后,进行额外的模式配置 // 1. 明确设置为同步主模式(虽然配置结构体已设置,但再次确认寄存器) USART0->CFG |= (USART_CFG_SYNCEN_MASK | USART_CFG_SLAVEEN_MASK); // 使能同步模式,禁用从模式(即主模式) // 2. 启用连续时钟输出 USART0->CTL |= USART_CTL_CC_MASK; // 设置CC (Continuous Clock) 位为1USART_CFG_SYNCEN_MASK: 该位置1使能同步模式。USART_CFG_SLAVEEN_MASK: 该位置0表示主模式,置1表示从模式。我们这里需要主模式。USART_CTL_CC_MASK: 这是实现“连续输出无需软件干预”的关键。该位置1后,只要USART的时钟源被激活,时钟就会持续从SCLK引脚输出,而不需要你通过写入数据寄存器来触发发送。如果没有这个设置,USART只会在发送数据时才会产生时钟脉冲。
4.5 第五步:启动USART0时钟输出
所有配置都完成后,最后一步是给USART0模块“上电”并选择时钟源。LPC802中,USART的时钟源选择是一个独立的寄存器UART0CLKSEL。
// 3. 选择USART0的时钟源,从而启动模块和时钟输出 // 首先,确保USART0模块本身被使能(上电) CLOCK_EnableClock(kCLOCK_Usart0); // 然后,选择时钟源。这里选择`frg0clk`,它默认就是由main_clk驱动的。 // 查阅参考手册,`frg0clk`对应的选择值是0x2。 SYSCTL0->UART0CLKSEL = 0x2; // 选择frg0clk作为USART0的时钟源 // 注意:复位后,UART0CLKSEL的默认值是0x7,表示“无时钟”,因此模块不工作。 // 将其设置为一个有效的时钟源值(如0x1, 0x2等),模块即开始工作,时钟开始输出。SYSCTL0->UART0CLKSEL这个寄存器是关键开关。在复位状态下,它是0x7(无时钟),USART模块处于“停滞”状态。当我们将其设置为一个有效的时钟源编号(例如0x2对应frg0clk),时钟信号立刻会流入USART模块,并根据之前的配置,从PIO0_8引脚输出1.8MHz的方波。如果你想停止PWM输出,只需要将这个寄存器再次写回0x7即可。
4.6 完整代码整合与主函数
将以上所有步骤整合到一个工程的主函数中,代码如下:
#include "fsl_device_registers.h" #include "fsl_power.h" #include "fsl_clock.h" #include "fsl_swm.h" #include "fsl_iocon.h" #include "fsl_usart.h" int main(void) { // 1. 芯片级初始化:解除复位,配置时钟 POWER_DisablePD(kPDRUNCFG_PD_IRC_OUT); // 使能内部IRC(如果需要) POWER_DisablePD(kPDRUNCFG_PD_IRC); // 使能内部IRC BOARD_BootClockFRO18M(); // 配置系统主时钟为9MHz // 2. 初始化引脚:将USART0_SCLK映射到PIO0_8 BOARD_InitPins(); // 3. 配置USART0:同步主模式、1.8MHz频率、连续时钟输出 USART0_Configuration(); // 这个函数包含了4.3和4.4步的所有配置 // 4. 启动USART0时钟输出 CLOCK_EnableClock(kCLOCK_Usart0); SYSCTL0->UART0CLKSEL = 0x2; // 选择frg0clk // 5. 主循环:此时PWM已经在硬件上持续输出,CPU可以休眠或执行其他任务 while (1) { // 这里可以添加其他应用代码 // 例如,让芯片进入低功耗模式以省电 // __WFI(); // 等待中断 } }将代码编译、下载到LPCXpresso802开发板后,用示波器测量PIO0_8引脚,你应该能看到一个稳定的1.8MHz、50%占空比的方波信号。
5. 调试技巧、常见问题与扩展应用
5.1 示波器测量与验证
将示波器探头连接到PIO0_8后,你可能需要调整示波器的设置来获得稳定的波形显示:
- 触发设置:设置为边沿触发,选择上升沿,触发电平设置在1.5V左右(对于3.3V系统)。
- 时基:由于是1.8MHz信号,周期约555ns。可以将时基调到200ns/div或500ns/div,这样屏幕上能显示几个完整的周期。
- 幅值:垂直刻度调到1V/div或2V/div,确保波形在屏幕中央。
- 测量功能:使用示波器的自动测量功能,读取频率和占空比。理论上,频率应非常接近1.8MHz,占空比应为50.0%。由于时钟源和分频都是数字逻辑,实际测量结果会极其精确。
如果看不到信号,请按以下步骤排查:
- 检查供电和编程:确认开发板的电源灯亮,并且程序已成功下载。
- 检查引脚连接:确认示波器探头确实接触到了PIO0_8引脚和GND,最好用万用表测一下PIO0_8对地是否有3.3V左右电压(说明引脚已激活)。
- 检查代码配置顺序:确保
SYSCTL0->UART0CLKSEL = 0x2;是最后一步。如果先启动了时钟源,但USART的同步模式或连续输出模式还未配置,可能会输出不正确的信号。 - 检查时钟源:确认
BOARD_BootClockFRO18M()函数被正确调用,系统主时钟确实是9MHz。可以在代码中翻转一个GPIO并用示波器测量其频率来间接验证。
5.2 常见问题与解决方案
| 问题现象 | 可能原因 | 排查与解决方案 |
|---|---|---|
| 无信号输出 | 1. 引脚映射错误。 2. USART时钟源未启动。 3. USART模块未使能。 | 1. 检查SWM_SetMovablePinSelect函数参数,确认映射到了正确的引脚(如P0_8)。2. 检查 SYSCTL0->UART0CLKSEL寄存器值是否为0x7以外的有效值(如0x2)。3. 确认 CLOCK_EnableClock(kCLOCK_Usart0);已被调用。 |
| 输出频率不正确 | 1. 系统主时钟频率不对。 2. BRG值计算或设置错误。 3. USART模块时钟源选错。 | 1. 验证系统时钟配置,确保FRO输出和分频设置正确。 2. 重新计算BRG值: BRGVAL = (Fclk / Desired_Freq) - 1。检查USART0->BRG寄存器的实际值。3. 确认 frg0clk的源是main_clk,且未经过其他分频。 |
| 占空比不是50% | 1. 示波器测量误差或触发问题。 2.配置模式错误(最可能)。 | 1. 尝试调整示波器触发电平和时基,使用高带宽探头。 2.务必确认 USART_CTL_CC位已被置1(连续时钟模式),并且USART配置为同步主模式(SYNCEN=1且SLAVEEN=0)。在非连续模式下,时钟只在发送时产生。 |
| 波形有毛刺或抖动 | 1. 电源噪声。 2. 探头接地不良。 3. 引脚负载过重。 | 1. 确保开发板供电稳定,远离噪声源。 2. 使用探头的接地弹簧,缩短接地回路。 3. PWM输出引脚不要直接驱动大容性负载,必要时串联一个小电阻(如22-100欧姆)。 |
5.3 方案扩展与灵活性
这个方案的优势不仅在于解决奇数分频问题,更在于其灵活性和可扩展性。
- 动态频率调整:虽然我们演示的是固定频率输出,但你完全可以在运行时动态修改
USART0->BRG寄存器的值来改变PWM频率。只要在修改前确保时钟输出已停止(UART0CLKSEL=0x7),修改BRG值后再重新使能时钟源,即可输出新的频率。这比重新配置定时器要简单直接。 - 多路PWM输出:LPC802有两个USART模块。你可以用同样的方法配置USART1,从另一个引脚输出第二路独立频率的50%占空比PWM。这对于需要多个不同时钟基准的应用非常有用。
- 非50%占空比?本方案核心是生成50%占空比。如果你需要其他占空比,这个方案本身不直接支持。但你可以将其作为一个精准的50%基准时钟,再用一个简单的GPIO翻转(通过另一个定时器中断控制)来对其进行门控,从而产生其他占空比的信号,不过这需要CPU干预。
- 更低频率输出:公式
U_SCLK = Fclk / (BRGVAL + 1)中,BRGVAL是一个16位寄存器,最大值65535。这意味着在9MHz主时钟下,最低可以输出约137Hz的PWM信号。对于很多低频应用也足够了。
通过这次对LPC802 USART模块的“非典型”应用,我们跳出了定时器的思维定式,利用通信外设的时钟生成单元,优雅地解决了一个特定的PWM生成难题。这种深入理解外设底层原理,并灵活运用的能力,正是嵌入式工程师从“会用”到“精通”的关键一步。在实际项目中,当标准路径走不通时,不妨多翻翻数据手册,也许某个不起眼的功能寄存器,就是打开新世界大门的钥匙。
