Skip to content

CDFOC 代码说明 上篇 之 bootloader

dukelec edited this page Jun 26, 2024 · 8 revisions

我们的代码工程是由 STM32CubeMX 自动生成的 Makefile 工程,调试手段以 printf 打印为主。

关于调试:

目前,除了使用 printf 打印调试,万一遇到小概率死机等问题,可以参考这个文章,使用 jtag 查看 backtrace: https://blog.d-l.io/stm32g0-hardfault-debug-cn
为何说 printf 打印比单步调试更先进也请看上面的文章。printf 除了打印到专门的调试串口,也可以通过用户 RS485 接口打印到上位机调试窗口。
对于 M4 核,应该是可以在 HardFault_Handler 调用 _Unwind_Backtrace 显示函数调用关系,方便排查一些小概率死机的问题。
早在我使用 STM32 F1 和 F4 系列芯片的时候,有这样操作过,后来使用 G0 系列 M0+ 内核,由于内核精简过,不支持 backtrace,所以后来使用 G4 系列也默认没有在 HardFault_Handler 中添加 backtrace 调试。
如果想尝试 backtrace 打印,可以参考 cdnet/utils/cd_list.c 结尾的代码,使用 gcc 自带的 _Unwind_Backtrace.

关于编译环境:

Makefile 工程可以方便用户简化开发环境的搭建,因为 IDE 有很多家,我如果选择其中一家,只能方便一部分用户,而给其余的用户带来不便。
而 Makefile 工程编译方便,且是跨平台的,只需要在 Arm 官方网站的 "GNU Arm Embedded Toolchain Downloads" 页面下载您系统相应版本的 gcc-arm-none-eabi, 然后安装 make 工具,最后敲一下 make 命令,固件就编译好了。
而且学习 makefile 和 gcc 知识对上位机等其它程序开发,以及对后续学习嵌入式 linux 也有帮助。
能使用 linux 为主系统环境则更加理想,10 年 linux 使用经验和 10 年 windows 使用经验哪个更有价值我不用多说。
如果您还是想使用其它 IDE 开发环境,可以参考这篇文章:https://github.com/dukelec/cdstep/wiki/改用-Keil-MDK-编译工程

代码目录结构

bootloader 工程位于 mdrv_bl 目录下,是一个独立的工程。
其中,cdnet 和 usr 是我们添加的,其它是 STM32CubeMX 自动生成的(后面简称 cube)。
还有一个 flash.sh 也是我们添加的,用于 st-link2 硬件烧录代码,首次烧录要烧录 option 字段,见 flash.sh 内的注释。
烧录工具使用的也是开源的命令行工具,支持 linux 和 windows:https://github.com/stlink-org/stlink

cdnet

cdnet 本身是一个面向 cdbus 的可选的上层协议,中文说明见:https://github.com/dukelec/cdnet/wiki/CDNET-协议中文版
命令示范见:https://github.com/dukelec/cdnet/wiki/CDNET-协议简介及示范

cdnet 目录是 cdnet 协议的一个 c 语言的库,除了包含 cdnet 协议本身,还包含 cdbus 协议的相关内容,以及一些通用的功能,譬如链表的实现。

子目录 arch 下用到 stm32 下的一个头文件 arch_wrapper.h, 其中包含了一些基础的封装,譬如 gpio、spi、i2c、systick,为了方便跨平台移植。
其中还有常用的 local_irq_save 和 local_irq_restore 函数,部分用户可能比较陌生,linux 内核中也有同名的函数,用途是开关全局中断,只不过如果原本中断就是关闭的,那么执行一次关闭中断再恢复后,中断开关会恢复到原本的状态。为了避免这种情况:原本中断是关闭的,我们执行一段代码需要关闭中断,如果我们先关闭中断,再直接打开中断,那么就很可能会坏事。

子目录 dev 目录下有 cdbus 的一些驱动,包含普通 uart 和 cdctl 专用硬件控制器,后者有两个版本,一个是 mcu 阻塞查询控制器状态以及收发数据包,而带 _it 的版本是非阻塞中断和 spi dma 配合的方式(_it 是中断的缩写)。bootloader 使用的是阻塞的版本,因为不需要运行其它任务,且要让 bootloader 保持简洁小巧。

dev 目录下的 cdbus.h 头文件包含了 cdbus frame 数据帧的定义,以及包含了不同 cdbus 硬件的驱动通用接口。

cd_frame_t 开头第一个成员 list_node_t node 是一个链表数据对象,这里使用了 container_of 这个核心技术(同样来自 linux 内核),这个技术可以通过一个 struct 内部的成员的指针,反推出所在 struct 的指针。这也是用 c 写面向对象程序的核心所在。(如果这个成员是在 struct 开头,其实成员指针和 struct 指针是相同地址,编译器也会自动优化,不会有多余 cpu 开销。)

cdbus.h 中的 cd_dev_t 也是上述面向对象编程的体现,cd_dev_t 是基类,不同的 cdbus 硬件驱动基于它派生出各自的派生类,譬如 cdctl.h 中的 cdctl_dev_t 包含 cd_dev_t 成员,上层程序通过通用的基类 cd_dev_t 的成员函数访问实际的驱动程序,驱动程序中定义的相关函数,譬如 cdctl.c 中的 cdctl_get_free_frame 函数开头,把基类对象 cd_dev_t 转换为派生类对象 cdctl_dev_t,然后再执行 cdctl.c 驱动的相关具体事务。

usr

app_main.c

这里面同名函数 app_main 是我们的入口函数,从 cube 自动生成的 main 函数结尾处跳转过来。

while true 循环里面,把 2 秒钟平均划分了 2 段 1 秒钟的时间,前 1 秒钟默认使用 115200 波特率,后 1 秒使用用户设置的波特率。

如果用户设置的波特率不是 115200,那么前 1 秒如果使用 115200 输出一些打印信息,那么会干扰到总线的正常通讯,所以第 1 秒钟保持静默。

无论是第 1 秒还是第 2 秒,只要收到主机发来的保持命令(csa 写寄存器 keep_in_bl),就会停下来,继续等待主机升级等命令。

否则 2 秒钟总时间到了,就会自动跳转到 app 固件执行。目前不检查 app 固件是否存在,直接跳转。

app_main.h

开头有 csa_t 的定义,以及它的两个实例 csa 和 csa_dft,这是用于寄存器读写的配置和状态表。

csa_dft 的内容是存放初始值,即便用户修改了 csa 参数,依然可以读取初始值用做参考。

配置表开头有一个 conf_ver 版本信息,高字节的大版本号,低字节是小版本号,bootloader 只判断大版本号一致即可使用该配置表。

cd_config.h

这是用来配置 cdnet 库的一个配置文件,cdnet 库编译的时候,会通过编译工具查找并引用这个头文件。

debug_config.h

这里存放的是真实串口调试打印的底层接口实现。被 cdnet/utils/cd_debug(_uart).c 调用。

config.c

这里是读写 flash 的实现。

common_services.c

这里是 cdnet 的相关端口的具体实现,包含设备信息读取、配置表读写、flash 擦除和读写。