找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 3721|回复: 17
收起左侧

关于STM32嵌入式程序调试的体会

  [复制链接]
ID:599678 发表于 2020-3-30 11:21 | 显示全部楼层 |阅读模式
工作5年,野路子。由感而发,随便写点,混点积分。
工作之后已经很久没有在论坛上活跃了,一方面没有时间,另一方面有问题时直接联系厂家技术支持了,很少在论坛里面请教东西。近来比较空闲,分享下自己写的一些代码方法。相信很多人初学 STM32是参考原子哥的例程的,为了方便大家理解,以下所写就在原子哥基础上加上一些自己的东西,当然很多也不定是我的东西,只不过掌握了而已。

就先,这个大家想必大家都会。就直接开门见山了,说下下面两点,串口发送和封 装串口功能。对于串口发送,下面是原先的:

这种串口发送在波特率不是很高时不推荐这样写,就以波特率 9600 来算,发送 1 位就需要
104us,发送一字节(8 位数据+1 位起始位+1 位停止位)就需要 1ms,可以看到在发送的 时候 MCU 是处于一直等待的状态,如果发送 100 个字节,就在这里等待 100ms 以上,在项 目中使用显然不合适。

实际在用的过程中,是建立一个状态标志,用来表示串口发送是忙还是空闲,当我们发送置 为忙,发送完成置为空闲。利用串口发送中断避免原子例程中的while 等待。如下面:


对于没有使用过串口发送中断的,可以自行搜索,这里不做过多解释,总之这种串口发送不 会让主程序一直等待硬件标志,当然这种发送方式需要上层应用做出判断,就是在当前数据 没有发送完前,不能发送新的数据。原先的写法可以连续发送很多条字符串,现在需要写个

状态机,判断当前串口是否忙,然后才能进行下一步操作。例如有 10 个数组,把 10 个数组 分别通过串口发送出去,每个数组发送完需要间隔 10ms 才能发送下一个数组。要实现这样
的功能,当然可以用一个 for 循环搞定这个操作,但是如果在不影响实时性的基础上实现, 这个会在后面的程序框架中讲到。

串口发送就到此为止,接下来讲下封装串口功能。对于一个多人维护的项目来说,并不需要 每个人都把全部代码或者别人写的代码都一一摸清楚,一个串口,我需要的就是很简单的功 能,打开串口,串口来数据里怎么办,串口发送完数据了怎么办。就像下面这种:

这是一个函数申明,前三个参数是需要打开的串口号、波特率、校验,第四和第五个参数是 函数指针,函数指针就是一个指针,只不过指向的是函数而已。这里来说明一下函数指针的 用法:
可以看到,函数指针可以指向一个同类型的函数,并且可以运行这个函数指针指向的函数。 知道这些接下来就可以亮出代码解释函数申明里面的两个回调函数了。

先从底层说起,首先申明一个结构体类型,

然后定义这个结构体变量,这个结构体分别对应 5 个串口的基本信息:

以串口 1 的中断函数为例,通过红色框中可以看出,每接收一个串口数据,会调用对应结构 体变量的 rx_call_back 函数指针。每发送完串口数据会调用结构体变量的 tx_call_back 函数
指针。

可以看到,这两个函数指针指向的函数已经被执行,就差一点了,这个函数指针指向的是哪 个函数?好了回到串口初始化函数,如下:


可以看到所指向的函数是通过 uart_open 传递过来的,具体指向哪个函数交于上层调用
uart_open 的来指定,底层的工作先到此结束(为了使代码好看,把串口 2、3、4、5 及引脚 配置都省略了,写这篇的意义不在意代码,而是思路)。

底层再补充一下串口发送及状态获取的代码吧:


这下底层的工作真的到此结束了。再从上层角度看下怎么使用这个函数,比如通过串口 1 和

NB 模组进行通信,下面就是一个打开串口的函数:


下面是串口回调函数:

可以看到上层程序设计相对来说已经不再过多涉及 STM32 本身的操作了,这种回调函数的 用法,使得程序分层设计,思路上更加清晰,方便维护。

再说,也是开门见山,直接说自己的两点,封装定时器和如果使用定时器。 先说如何封装定时器吧,其实也就是回调函数,和上面封装串口一样。这里就直接晒代码了,
下面是底层代码:



下面是上层代码,和封装串口类似,上层只要知道打开一个定时器,多长时间进入自己定义 的定时器中断就可以了。

接下来就是如何使用定时器了,我这定时器可能和你想的不一样,这里主要介绍一下思路。 前面都是先从底层说起,这里换个角度先从上层说起,对应上层的设计人员,该人员需要的

仅仅是定义一个变量,然后注册到定时器里。如下面:


当上层人员调用 time_cnt_reg 函数,实际就已经注册一个定时器了,比如 rx_over_time 就会 每 1ms 增加 1,inq_reg_over_time 每 1s 增加 1。上层人员知道这么用就可以了,当然也许 你会好奇,对这个变量进行查找,并没有对该变量的操作,怎么就增加了呢。

从接下来开始,所说的操作也不算是底层操作了,应算是整个程序设计中的框架功能。以下 就从框架功能角度说了,主要就是 time_cnt_reg 是怎么一回事。 其实也很简单,定义一个链表,链表中就一个时间单位,是 ms 还是 s,然后就是一个指针, 指向的是待注册的变量。



每次调用 time_cnt_reg 函数,就会向链表中增加一个成员,成员中的 cnt_ptr 指向 time_cnt_reg中带入参数地址。

下面就是对链表中的成员进行自加 1 操作,

上面这两个函数在哪里使用的呢,回到封装定时器操作,里面的回调函数做了什么?

这下应该清楚了注册定时器是怎么一回事了。我这里用的注册定时器用的链表方式,也可以 改成结构体数组。以前我也用过结构体数组,结果有次注册的定时器超过定义的上限,结果 排查了半天才找到原因才改的使用链表。

好了,再回到上层应用,注册好定时器后,应用起来就和正常使用的一样就可以了,可能你 会说既然用起来都一样,为何你这还要多此一举,这个变量完全可以在定时器中断里面直接 使用 rx_over_time++搞定的事么。这个么仁者见仁智者见智,有兴趣的可以接着往下看。


再说,很多人喜欢上一些实时操作系统,在我看来其实没有必要,那些实时操作 系统可以当做学习 LINUX 的过渡,但使用起来没觉得哪里特别的。自己搭一个抽象系统+状
态机就可以搞定的事情,程序设计更加健壮、可控、高效。这里的抽象系统就算是我所说的 程序框架吧。

先说,通过 app_reg 注册一个任务,思路和前面使用定时器差不多,每次 app_reg 执行都会向链表中添加一个节点,然后在定时器回调函数中对该APP 时间计数加 1 操作。 为了节约篇幅这里就不贴出这部分代码了。

主函数的代码很短,主要就是 app_init 和 app_manage,查看源码发现仅仅有 2 处区别, app_init 函数不需要对应的计数器到达设计值就可以,但是app_manage 需要计数器到达设 定值才执行。其次 app_init 执行时带入的参数为 0,app_manage 执行时带入的参数为 1。



下面以内部看门狗的示例展示一下用法:


可以从代码中看出,data 为 0 时即初始化看门狗,为 1 时即每 50ms 执行一次,我个人觉得 这么写的话代码紧凑一些,方便查看。看过很多裸跑的程序,建立几个时间标志,10ms 到 了干什么什么事,1s 到了干什么什么事,结果同样是一个事情,这里一行代码,那里一行代 码,时间长了就不知道还有哪里会有这些代码。

再以一个例子引入下一个程序功能吧,485 总线收发数据,当需要发送 485 数据时,先将控 制收发引脚置为发送,然后将串口数据发送出去,然后稍微加些延时,再然后将控制引脚置 为接收。我想很多人操作这个时应该是让 MCU 硬等待在这一块。看了我上面写的串口代码, 可以这样写,当发送时,先将 485 置为发送,然后发送数据,在串口发送完成回调函数中将
485 再置为接收。 然后实际测试却不是这样,发送串口数据完成然后在回调函数中立刻置为接收,会造成 485 最后一个字节数据发送不完整,在回调函数中加入延时再置为接收就没有问题,但是这里的 延时,实际上就是在串口发送中断函数执行的,很明显不合理。如何解决这个问题,程序框 架中又加入一项功能:长时。具体设计思路不再贴出 来了,实现这一功能和前面的都差不多,这里只说下怎么应用的,con_485_recv 是一个控制
485 为接收的函数,在串口发送完成回调函数中执行 fun_once_ms_late_reg(10, con_485_recv);

即 10ms 后执行 con_485_recv。为了避免 485 连续发送几条数据时,之前的 con_485_recv 到 时间了执行,在发送数据时除了置为发送,还要调用fun_once_ms_late_unreg(con_485_recv),
即取消还有多长时间后执行的 con_485_recv,避免时序错误。

再说,以操作 NB-IOT 工作在主动上报类为例,相信很多人看过原子的操作 AT
指令的函数,如下:


然后就是调用这个函数一个个执行 AT 命令,这种写法在例程里作为实验是可以的,但是实 际应用不该这么写,影响系统的实时性,也难以维护。下面是操作NB 模组工作的流程图, 按照如下流程进行程序设计。



下面是对 NB 状态的相关定义、变量申明及初始化为上电流程

下面就是主要的处理了,我们让 nbiot_proc 运行的时基为 50ms,AT 命令发送处理中,不同 AT 指令响应的时间不同,有些需要几百毫秒,有些需要几秒,就通过 nb_work.delay 实现发 送处理的延时处理,这里的延时并不影响系统的实时性。另外对比发送处理和接收处理,可

以发送不同的流程下每个流程都公用一个处理函数,只不过带入的参数不同,我觉得这样写 便于阅读而已。



以上电初始化代码参考如下,可以看到,首先断电并等待 2s,然后上电等待 5s,然后置为
参数初始化流程,在这 5s 期间,如果收到模块发出的对应信息,提前结束等待时间,并且 转到下一个流程。其实通过这一个流程就已经差不多表述了我所想表达的状态机写法,这样 的写法有两个好处,一是实时性保持良好,二是通过 state 和 process 就可以知道当前模块 工作在什么状态哪个流程,做到“可控”。


下面是对接收到的消息处理,主要就是把 NB 收到的消息放入消息队列里(稍微有些删减):


下面是发送消息的处理,其实就是申请一个消息队列 上层只管对消息队列进行处理,而消息队列的处理,是通过下面的这个函数进行处理。其实
这个并不是队列,只是一种延伸的用法,不过习惯了这么叫。


再说下一些微小功能吧,主要是方便而已。调试,这个可以用来和 MCU 进行一
些 SHELL 指令交互,比如板上有个 NANDFLASH 或者 SD 卡,搭载文件系统,想要看里面文 件时,总不能每次都把卡拔出来看吧,这时可以通过调试串口进行命令交互,就像下面一样, 当然这部分程序要写:

当然还可以有其他功能,比如查看任何一个外设的状态,查看某个串口交互的全部报文等,

就看应用者如何赋予功能。总不能想看下串口收发报文,用个 USB 转 TTL 焊在对应的引脚 上吧,太不方便了。



LOG,批量的东西,经常有很多难以复现的问题,这些问题很多是在特定的情况 下触发的软件 BUG,这种情况下,如果有 LOG 功能,就方便分析问题了,但是如果没有那 也只能靠猜了。导出 LOG 也可以通过调试串口来实现,总之就是为了方便。


再说,其实这一块感觉也没啥写的,设计思路是这样的,对于任何一个模块,比 如 NB 模块和对 NB 消息队列的处理,每个都向睡眠机制中注册一个变量,NB 底层状态机 处理只要不是空闲,对应变量都为真,NB 消息队列只要有未处理的数据,对应变量也为真。 睡眠管理在 while(1)里,检查所有注册的变量,当所有的变量都为假时,调用注册的回调函 数并进入睡眠,然后多长时间唤醒一次再通过另一个回调函数进行某项处理。下面是应用在 华大一款低功耗芯片的 main 函数:


对应的低功耗还有时钟切换,也是向时钟管理机制中注册一个变量,比如某段时间只有几个
led 亮着,那就用 32768Hz 的时钟做主频,如果有用 IIC 的,就用 4M 做主频等等,while(1) 都是对注册的变量进行判断管理。 其实之前也没有搞过太多低功耗的东西,只是不想破坏原有的各种外设的代码及风格,便这 样处理了。下面是睡眠管理处理,涉及到对底层寄存器的处理和芯片的工作模式,看过一段 时间现在忘了。时间长了我都不知道写的啥,只知道上层怎么使用的了,但这我觉得就够了。



先写到这边吧,看不明白没关系,因为我觉得自己写的都不知道写的啥,文笔太差,随便写写。

以上的pdf格式文档51黑下载地址(内含清晰图):
嵌入式软件随笔.pdf (1.37 MB, 下载次数: 36)

评分

参与人数 2黑币 +65 收起 理由
IdeaMing + 15 很给力!
admin + 50 共享资料的黑币奖励!

查看全部评分

回复

使用道具 举报

ID:599678 发表于 2020-4-1 08:46 | 显示全部楼层
已上传的文档好像不能再次编辑,可以通过GIT获取最新WORD版文档,不定期更新。有任何疑问此贴必回。
https://gitee.com/hubaojin/ARM_DEBUG_STUDY.git

评分

参与人数 1黑币 +50 收起 理由
admin + 50 回帖助人的奖励!

查看全部评分

回复

使用道具 举报

ID:722501 发表于 2020-4-4 21:07 | 显示全部楼层
感谢分享
回复

使用道具 举报

ID:722501 发表于 2020-4-4 21:07 | 显示全部楼层
感谢分享
回复

使用道具 举报

ID:405033 发表于 2020-4-4 22:13 | 显示全部楼层
受教颇多,谢谢分享经验
回复

使用道具 举报

ID:265975 发表于 2020-4-4 22:21 | 显示全部楼层
支持楼主
回复

使用道具 举报

ID:265975 发表于 2020-4-4 22:22 | 显示全部楼层
受教颇多,谢谢分享经验
回复

使用道具 举报

ID:721835 发表于 2020-4-5 17:48 | 显示全部楼层
感谢分享啊~学习了~
回复

使用道具 举报

ID:128463 发表于 2020-4-11 10:15 | 显示全部楼层

感谢分享!!
回复

使用道具 举报

ID:691681 发表于 2020-4-12 19:59 | 显示全部楼层
调理清楚,表达清晰准确!楼主加油!
回复

使用道具 举报

ID:399788 发表于 2020-4-25 01:41 | 显示全部楼层
学习了,谢谢楼主!!
回复

使用道具 举报

ID:737697 发表于 2020-4-25 14:10 | 显示全部楼层
学习了,谢谢分享
回复

使用道具 举报

ID:276663 发表于 2020-4-25 17:15 | 显示全部楼层
多谢分享,很有参考意义
回复

使用道具 举报

ID:142652 发表于 2020-4-26 11:14 | 显示全部楼层
干货满满哦,谢谢!
回复

使用道具 举报

ID:738622 发表于 2020-4-26 16:51 | 显示全部楼层
很有份量的分享,受教了
回复

使用道具 举报

ID:738950 发表于 2020-4-26 23:37 | 显示全部楼层
stm32正点原子和野火配合着看学习最好
回复

使用道具 举报

ID:739404 发表于 2020-4-27 16:04 | 显示全部楼层
厉害! 感谢分享
回复

使用道具 举报

ID:320751 发表于 2020-5-1 20:29 | 显示全部楼层
谢谢详细讲解
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|51黑电子论坛 |51黑电子论坛6群 QQ 管理员QQ:125739409;技术交流QQ群281945664

Powered by 单片机教程网

快速回复 返回顶部 返回列表