news 2026/8/30 22:22:26

STM32MP157输出PPS信号:硬件定时器与设备树配置实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32MP157输出PPS信号:硬件定时器与设备树配置实践

1. 这次 "(again)" 的背景:一块开发板上的 PPS 输出为什么反复翻车

先说结论:在 stm32mp157d-dk1 上输出一路干净的 PPS(Pulse Per Second,秒脉冲)信号,本身不算什么高难度动作,难的是你"以为做对了"和"实际真的对了"之间,隔着好几条不容易发现的沟。标题里那个 (again) 是我自己加的——这块板子的 PPS OUT 信号我断断续续折腾了好几轮,每次换一个思路,总有新的坑在前面等着。这次终于把从硬件选型、设备树、内核配置到 sysfs 操作和示波器验证的整条链路全捋顺了,所以把完整过程记录下来,给同样在这块板子上做授时、同步、PWM 输出的朋友一个可直接抄作业的参考。

PPS 信号说白了就是一个每秒出现一次、脉宽相对固定的脉冲,它的上升沿代表整秒时刻,是各种授时与同步应用的基本节拍。GPS 接收机、GNSS 驯服钟、PTP 网络同步、高速数据采集系统里都很常见。我这次要做的事情,是让 stm32mp157d-dk1 像一颗 GPS 模块那样,从某个 GPIO 上稳定输出这个 1Hz 脉冲,供后端的采集设备和对端仪器做时间对齐。

为什么选这颗芯片?STM32MP157D 是 ST 的异构 MPU,双核 Cortex-A7 跑 Linux,Cortex-M4 做实时控制,内部有大量通用定时器和高级定时器,理论上生成 1Hz 硬件方波并不费劲。但"理论上不费劲"和"实际调通"之间,隔着设备树、pinctrl、时钟树、内核配置、PWM 子系统等一长串环节,任何一个环节理解不到位,示波器上就是一片死寂。

这篇文章主要写给三类人:一是想在自己的 STM32MP1 板子上输出 PPS 或精确 PWM 的嵌入式工程师;二是做授时同步相关项目、需要搞清楚 PPS 生成精度的朋友;三是刚开始接触 STM32MP157、想知道 Linux 下 PWM 到底怎么用的入门者。我会把这一轮完整的排查链路和每一步的具体配置都写出来,包括我踩过的每一个坑和对应的排错方法,尽量让后来者少走弯路。

2. 硬件底牌与选型:哪些定时器、引脚能扛住 PPS 输出

2.1 定时器资源盘点:32 位与 16 位的差距对 PPS 是致命的

在 STM32MP157D 上,能直接通过硬件输出方波的资源主要是通用定时器 TIM2~TIM5、高级定时器 TIM1/TIM8,以及部分低功耗定时器 LPTIM。它们之间最关键的差异是计数器位宽:

定时器计数器位宽备注
TIM1 / TIM816 位高级定时器,有互补输出
TIM2 / TIM532 位通用定时器,最适合长周期 PWM
TIM3 / TIM416 位通用定时器,常用于电机、背光
LPTIM1~LPTIM416 位低功耗定时器,时钟源灵活

位宽决定了 Linux 的 PWM 驱动能否用"不分频"的方式去表达 1 秒周期。假设定时器输入时钟是 200MHz,1 秒就是 200,000,000 个计数;16 位计数器最多数到 65,535,必须配合预分频器。而预分频一旦介入,PWM 输出的"计数分辨率"就会从单个时钟周期变成 PSC+1 个时钟周期,边沿会出现量化误差。

为什么这个误差对 PPS 很重要?因为 PPS 的上升沿是要拿去对齐整秒时刻的。如果量化误差是几百纳秒甚至几微秒,在普通 PWM 场景里无所谓,但在授时同步场景里就直接废了。TIM2 或 TIM5 是 32 位计数器,可以做到 PSC=0 直接计数,200MHz 下理论边沿精度就是 5ns,这个指标对绝大多数 GNSS 驯服钟、PTP 边界时钟的场合都绰绰有余。所以我第一轮就把目标锁定在 TIM2 和 TIM5 上。

2.2 引脚选择:先看原理图,再谈 AF 映射

选完定时器,下一步是引脚。这里必须强调:动手之前先打开这块板子的原理图和 STM32MP157D 数据手册的 AF(Alternate Function)映射表,不要凭经验猜。

以 TIM2 为例,常见的输出通道引脚有:

  • PA0 —— TIM2_CH1(AF1)
  • PA1 —— TIM2_CH2(AF1)
  • PA2 —— TIM2_CH3(AF1)
  • PA3 —— TIM2_CH4(AF1)
  • PA5、PA15 在某些封装下也有 TIM2 通道复用

但我查了 DK1 的原理图之后发现,PA0 在这块板子上被用户按键占用。如果硬把它配成 TIM2_CH1,轻则按键失效,严重的情况下按键电平反向灌入定时器输出引脚,可能引起配置混乱。最后我选了 PA1 作为 TIM2_CH2 输出——PA1 在 DK1 上被引到了扩展排针,并且没有和板上任何外设冲突,是一个相对干净的选择。

选引脚时要同时确认三件事,缺一不可:

  1. 这个引脚在板级原理图上有没有被其它外设占用,比如 LED、按键、以太网 RMI、USB、音频 codec、SD 卡等;
  2. 该引脚对应的 AF 号是否真是你要的定时器通道,也就是查数据手册的 AF mapping 表;
  3. 该引脚所在 GPIO bank 的时钟域是否使能、有没有外部上下拉电阻影响电平。

这些信息在板级 DTS 里基本都能查到。比如 DK1 的 DTS 中,某个引脚如果已经被其它节点分配,你又在其它的 pinctrl 节点里复用它,内核启动时 pinmux 子系统通常会给警告。但更隐蔽的情况是引脚被 bootloader(U-Boot)先行配置过,Linux 启动后又重新编程,两边有冲突时外设行为会很诡异,这种问题最难查。

2.3 从 PWM 到 PPS:先搞懂两者的区别再动手

大多数人第一次碰到 PPS 会想:不就是产生一个 1Hz 的方波吗?对,也不对。PPS 和普通 1Hz 方波的关键区别在于对边沿时刻精度的要求。

普通 PWM 你可能只关心周期和占空比的大致范围,几百微秒的抖动无所谓。但 PPS 拿上升沿去对齐整秒,如果上升沿抖动达到几十微秒,后端的高精度时间同步系统直接就崩了。所以在选实现方式的时候,第一选择必须是硬件定时器,而不是软件翻转 GPIO。这也是我后续方案选型的核心逻辑:宁可多花时间折腾设备树,也要用硬件定时器去扛这个边沿精度。

3. 第一版方案复盘:软件翻转 GPIO 的抖动到底从哪来

3.1 我最初的想法:用实时线程反复翻转 GPIO

既然是双核 A7 跑 Linux,我一开始的直觉是:写一个内核线程或者用sched_setscheduler把它提到 SCHED_FIFO,然后在一个循环里gpiod_set_value()翻转引脚。这个思路其实代表了很多人面对"生成一个 1Hz 信号"时的第一反应,因为它最快,不需要改设备树,不需要碰内核配置。

代码逻辑很简单:线程里先睡到一个整秒边界,然后拉高 GPIO,延时 100ms,再拉低,接着睡到下一个整秒边界。为了对齐边界,我在代码里用了clock_gettime(CLOCK_REALTIME)读取当前时间,计算到下一个整秒的差值。理论上,如果nanosleep足够准、调度足够及时,输出应该是一个每秒一次、脉宽 100ms 的脉冲。

实测结果让人崩溃。示波器上看到的脉冲上升沿相对于整秒参考点,抖动范围在 ±300µs 到 ±2ms 之间,而且抖动分布一点都不规律。有时候连续几十个脉冲看起来还行,突然一个脉冲就晚了接近 2ms,没有任何征兆。

3.2 抖动的根源到底是什么

排查后我得出三条结论,这三条结论对任何想在 Linux 上做精确时序的人都有参考价值:

第一,nanosleep的精度取决于内核的高精度定时器(hrtimer)配置。如果内核没有开启高精度定时器支持,nanosleep的唤醒精度可能只有 1ms 到 10ms,这直接决定了软件方案的抖动下限。

第二,SCHED_FIFO 虽然能抢占普通进程,但抢占本身要等当前 CPU 上的中断处理完。网络、存储、USB 的中断随时可能来,任何一个中断处理耗掉几十到几百微秒,你的 PPS 线程就只能等着。

第三,也是最根本的:gpiod_set_value()的调用路径太长了。它要经过 GPIO 子系统的 sysfs 或 GPIOLIB 抽象层,一层层调用到最后才是寄存器写操作。这个路径上可能有锁竞争、可能有缓存未命中,每次调用的实际耗时并不确定。在几十纳秒级别就能完成的寄存器写操作,被软件路径放大到了微秒级别。

3.3 有没有救?hrtimer + busy loop 的极限试验

我不死心,又试了一个更极端的版本:用 hrtimer 把唤醒精度提到亚毫秒级,然后在用户态用clock_gettime(CLOCK_MONOTONIC)做自旋等待,等时间到达精确的整秒点就直接往 GPIO 寄存器写。这个方案理论上能把抖动压到几微秒以内。

实测确实比纯 sleep 方案好一些,抖动降到了大概 ±10µs 到 ±50µs。但还是不满足我的要求——后端设备对 PPS 边沿的容差是 ±1µs。而且用户态直接操作物理地址需要devmem或者写内核模块,这本身就不像一个"正经"的方案。到这里我彻底想明白了:只要边沿由软件指令触发,抖动就不可避免。必须让定时器硬件自己去翻转引脚,让边沿由硬件计数器的比较匹配事件产生。

4. 正确的打开方式:设备树 + Linux PWM 子系统的完整配置链路

4.1 先检查内核配置,别急着改设备树

在动手改设备树之前,先确认内核里 PWM 相关的支持有没有编进去。ST 官方 OpenSTLinux 镜像一般默认是带CONFIG_PWMCONFIG_PWM_STM32的,但如果你自己裁剪过内核,这几个配置项很容易被漏掉。

检查方法很简单:

zcat /proc/config.gz | grep PWM # 或者如果内核没有导出 config.gz grep PWM /boot/config-$(uname -r)

需要看到这几个关键的配置:

CONFIG_PWM=y CONFIG_PWM_STM32=y

如果没有,就需要重新配置内核,进入Device Drivers -> Pulse-Width Modulation (PWM) Support,把 STM32 timer PWM 支持选上,重新编译烧录。这一步是后面所有操作的前置条件,很多人折腾半天没有任何输出,最后发现内核根本没编 PWM 驱动,白白浪费一下午。

4.2 设备树修改:让 TIM2 以 PWM 模式接管 PA1

内核配置没问题之后,重点就是设备树。在 STM32MP157 的设备树里,定时器资源默认定义在stm32mp151.dtsi中,每个定时器下面挂着 PWM、trigger、encoder 等子节点。我们需要做三件事:把 TIM2 节点使能、把 TIM2 下的 PWM 子节点使能、给 PWM 指定正确的 pinctrl。

在板级 DTS(stm32mp157d-dk1.dts)里加入下面这一段:

&timers2 { status = "okay"; /* 防止 DMA 通道被其它驱动占用,PWM 模式用不到 DMA */ /delete-property/ dmas; /delete-property/ dma-names; pwm2: pwm { pinctrl-0 = <&tim2_pwm_pins>; pinctrl-names = "default"; status = "okay"; }; };

然后在&pinctrl节点里定义tim2_pwm_pins

&pinctrl { tim2_pwm_pins: tim2-pwm-pins { pins { pinmux = <STM32_PINMUX('A', 1, AF1)>; /* TIM2_CH2 */ slew-rate = <0>; }; }; };

这里STM32_PINMUX('A', 1, AF1)的意思是把 PA1 复用为 TIM2_CH2。AF1 这个编号必须和数据手册里的 AF mapping 一致。如果你用的是别的引脚或别的定时器通道,这一行的端口号、引脚号、AF 号都要跟着改。

关于/delete-property/ dmas/delete-property/ dma-names:这是一条容易被忽略但很重要的配置。STM32MP1 的某些定时器在默认 dtsi 里是带 DMA 通道描述的,而在 PWM 模式下我们不需要 DMA。如果留着 DMA 属性,某些内核版本下 PWM 驱动初始化时可能会因为 DMA 通道申请失败而整体加载失败,导致 PWM 子系统里根本看不到这个 timer。所以保险起见,先删掉。

改完设备树之后重新编译并烧录,或者如果你用的是 U-Boot 的 FIT image 流程,也可以把 DTB 一并更新。重启后检查 PWM 子系统是否识别到了新的 PWM 芯片:

ls -l /sys/class/pwm/ cat /sys/kernel/debug/pwm

如果设备树配置正确,/sys/class/pwm/下应该多出一个或多个pwmchipN/sys/kernel/debug/pwm里能看到 TIM2 对应的 PWM 芯片已经在列表里。

4.3 通过 sysfs 把 PWM 配成 1Hz PPS

一旦确认 PWM 芯片存在,接下来的操作就简单了——直接在 sysfs 里配置周期和占空比。PPS 脉冲的常见规格是上升沿对齐整秒、脉宽 100ms,对应到 PWM 参数就是周期 1 秒(1,000,000,000 ns)、高电平时间 100ms(100,000,000 ns)。

# 假设 TIM2 对应 pwmchip0,导出通道 0(对应 TIM2_CH1)或通道 1(对应 TIM2_CH2) echo 1 > /sys/class/pwm/pwmchip0/export # 设置周期为 1 秒(纳秒) echo 1000000000 > /sys/class/pwm/pwmchip0/pwm1/period # 设置高电平时间为 100ms echo 100000000 > /sys/class/pwm/pwmchip0/pwm1/duty_cycle # 使能输出 echo 1 > /sys/class/pwm/pwmchip0/pwm1/enable

如果一切顺利,拿示波器探 PA1(或者你选的引脚),应该能看到一个完整的 1Hz 方波,高电平 100ms、低电平 900ms。用示波器自带的频率计和脉宽测量功能验证一下,周期误差在示波器分辨率范围内几乎为零。

这里要特别解释一下数据是怎么算出来的:periodduty_cycle的单位都是纳秒。1 秒是 10^9 纳秒,100ms 是 10^8 纳秒。内核 PWM 驱动在收到这些数值后,会结合定时器实际时钟频率自动计算预分频值和计数比较值。对 TIM2 这种 32 位计数器,1 秒周期根本不需要预分频,计数精度直接就拉满。

4.4 为什么说这是"正确"的方式

对比一下软件翻转 GPIO 和硬件 PWM 的本质差异:软件方案里,边沿出现的时刻取决于 CPU 何时执行到那条翻转指令,这中间有调度延迟、中断抢占、缓存失效等一系列不确定因素;硬件 PWM 方案里,边沿由定时器计数器的比较匹配事件触发,计数器硬件自动翻转引脚,和 CPU 执行什么代码、系统负载有多高完全无关。

用 200MHz 定时器时钟计算,计数器每 5ns 跳动一次,比较匹配的精度就是这一个时钟周期,

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/30 22:17:39

STM32H533 OEMiROT安全启动实战:non-secure工程与AXI配置详解

1. 项目解析与整体设计思路 1.1 一个“看似普通”的non-secure工程&#xff0c;背后是整个信任链 第一次接触“STM32H533 OEMiROT non secure project”这个需求时&#xff0c;我差点把它当成一个普通的STM32CubeMX工程来处理——选个芯片、配个时钟、点两盏灯、编译下载完事。…

作者头像 李华
网站建设 2026/8/30 22:12:37

LLM Agent失败实时检测与自动修复:从事件流到多级策略的工程实现

前几天排查一个线上数据采集 Agent 的连环报错时&#xff0c;我盯着日志里反复出现的工具调用失败提示&#xff0c;突然意识到一个问题&#xff1a;我们一直在监控接口的 5xx、数据库的连接数、模型的响应延迟&#xff0c;却很少有人真正监控 Agent 的“行为轨迹”——它做了哪…

作者头像 李华
网站建设 2026/8/30 22:11:48

C++实现带实时预览的Markdown编辑器:架构与构建指南

这次我们来看一个很有意思的本地工具方向&#xff1a;用 C 实现带 live preview 的 Markdown 编辑器。市面上主流的 Markdown 编辑器大多基于 Electron、TypeScript 或者 Python&#xff0c;天然带着比较重的运行时依赖&#xff0c;启动速度和内存占用都谈不上理想。相比之下&a…

作者头像 李华
网站建设 2026/8/30 22:11:22

大客户应收集中度过高,销售团队如何用五力改善客户结构

从客户组合看大客户销售的风险 大客户销售最怕的不是订单不够&#xff0c;而是客户结构过于集中。当个别客户的应收占比过大&#xff0c;销售团队容易把稳定误认为安全。PSS⁵ 从5个维度帮助销售管理者重新审视这类问题。 价值力要求把客户收入、毛利、账期和坏账风险放到同一张…

作者头像 李华
网站建设 2026/8/30 22:09:35

Python接单的真实门槛:从需求沟通到交付维护的完整链路解析

最近经常刷到类似标题&#xff1a;“准大学生在家做Python接单&#xff0c;两个月2.8w&#xff0c;已实现经济自由&#xff0c;一台电脑&#xff0c;方法简单&#xff01;&#xff01;&#xff01;”这类帖子在短视频平台和社区里热度很高&#xff0c;评论区比标题本身还热闹&a…

作者头像 李华
网站建设 2026/8/30 22:06:01

Codex Skills 实战指南:8个必备技能安装、配置与自定义编写

最近在本地环境里集中验证了一批 Codex Skills&#xff0c;踩了不少安装、加载和调用上的坑。网上的资料大多是零散片段&#xff0c;有的讲安装方法&#xff0c;有的只给 SKILL.md 模板&#xff0c;缺少一份“装上之后到底能干什么、实际效果怎么样”的完整记录。所以这篇文章我…

作者头像 李华