OpenOCD刷写FLASH代码结构浅析(基于RISCV)

1.配置文件中对应命令的分析

我们一般在使能OpenOCD的刷写功能的时候,需要在OpenOCD的cfg文件中添加如下两条语句:

(1) $_TARGETNAME configure -work-area-phys 0xaaaaaaaa -work-area-size 0xbbbbbbbb -work-area-backup 0
(2) flash bank spi_flash fespi 0xcccccccc 0 0 0 $_TARGETNAME 0xdddddddd

这两条语句会在配置参数的同时,注册一系列的handler函数.
对于第一条语句的解析:下面的图是OpenOCD中的代码调用栈, 可以看到 -work-area-phys字符串后的数字会被解析出来,放入target->working_area_phys变量中,该变量存放的是Core上一个物理起始地址,同样-work-area-size后面跟的是未来要以这个物理起始地址开始分配的一个空间的大小; -work-area-backup决定了是否要对Core上那个空间的数据进行备份(但我感觉没有大的用处,OpenOCD一般都会做备份动作).

对于第二条语句的解析:大家如果看过我的另一篇讲解OpenOCD代码结构的文章应该清楚,OpenOCD在启动的时候注册了一系列的函数handler,主要分两大类:一类是为了解析config文件中的语句而注册的handler函数,另一类是为了在OpenOCD运行阶段解析输入进来的命令而注册的handler函数. 可以看到由于第二条语句中包含"flash bank"字串,因此通过遍历下面图上显示的定义关系,预先注册的handler函数handle_flash_bank_command()就会被调用到. 用它来解析第二条语句后续的字段.

下图是函数handle_flash_bank_command()的调用关系,这里CMD_ARGV[]中保存的就是第二条语句中的后续字段.依靠字符串"fespi"从全局表flash_drivers[]中找到对应的flash driver,这个表中包含各种的flash-driver,是根据名称进行查找的,其中的"&fespi_flash"项就是工作在RISCV平台上的项. 找到该项后,通过register_commands()函数将其中的函数进行注册.在函数的最后,将新建立的flash_bank{}实例加入到全局变量flash_banks链表中,它的name就是我们在命令里面定义好的"spi_flash",后续要搜索这个链表时,就是通过name来查找的.

2. 执行命令的handler函数的注册

我们说过,在OpenOCD中会有两类handler函数会被注册,刚才讲的config类的handler函数部分,到这里就该涉及到flash相关的执行类handler函数的注册了. 如下图,因为flash_exec_command_handlers[]实在太长,我只能截取一部分放在这里,而且大家也注意到了后面还有一张截图是flash_init_drivers()函数,它里面会对这个数组进行注册.

那么flash_init_drivers()函数在哪里被调用呢? 看下面的截图, openocd_main()函数已经属于比较顶层的函数了,属于比较早的启动代码了.它做了两件事情: 一个就是调用setup_command_handler()函数,主要完成对于config类handler的注册;另一个就是调用openocd_thread()函数开始主线程循环,同时进行执行handler函数的注册.它会调用handle_init_command()函数,在该函数中会有专门调用"flash init"字符串相关的handler函数.

下面的截图就是关于handle_flash_init_command()怎么进行注册的一个关系,当然它是在config类的handler函数注册时就完成的工作. openocd_thread()-->handle_init_command()-->command_run_line("flash init")的调用关系使得flash_exec_command_handlers[]中的函数都得到了注册.

3. 开始讲解FLASH刷写前的准备工作

第一个预备知识: 在开始将image文件中的数据刷写到flash上之前,OpenOCD自己需要完成一个重要的工作,那就是编译出一个可以运行在目标处理器上的小程序,这个小程序很简单,主要的逻辑就是操作处理器中关于SPI的一组寄存器,将来自OpenOCD的image数据通过SPI总线写入到FLASH中. 对于RISCV平台来讲,目前它是放在这个路径下的:

riscv-openocd/contrib/loaders/flash/fespi/

它最终会生成两个版本的binary文件: riscv32_fespi.incriscv64_fespi.inc分别对应32位与64位的版本.这两个bin文件可以在编译OpenOCD工具的时候自动生成,也可以在该目录下手动编译生成. 打开它里面的Makefile文件,很多的疑问迎刃而解: 首先它使用了"-fPIC"的编译选项,保证了生成出来的代码是与地址无关的,那么不管这个代码下载到哪个地址边界处,它都是可以正常运行的. 其次它使用了 "-Obinary"的选项,将ELF文件不需要的垃圾信息都清除掉,只留下最小最干净的binary. 最后它使用了 "-nostdlib"以及"-nostartfiles",因此它自己写了一个很小的入口汇编文件"riscv_wrapper.S"作为替代. 这样一个干净小巧的可运行在RISCV平台上代码就完成了.

再多说几句关于这个运行在处理器上的小程序的处理逻辑. 打开它的文件"riscv_wrapper.S"如下图所示, 因为代码很短我就直接全部贴出来并加以说明: 首先stack的空间它是已经开辟好了,在程序的入口首先设置好了栈寄存器sp的值,为以后进行函数调用做准备;在下面截图的第15行进行了函数调用,进入到函数flash_fespi()中,这个函数是这个片上小程序唯一的主体函数,刷写FLASH的工作就是在这里完成的.注意它的入口参数有六个,一定要记住,后面我们会讲到. 在下面截图的第16行有个ebreak指令,这个非常关键. 因为flash_fespi()函数不是个死循环,在完成flash的刷写工作后,它会从该函数中返回(当然返回时,"a0"寄存器中带回了刷写过程中的状态). 为了让OpenOCD知道这个刷写的动作已经完成,也为了防止片上程序乱飞,此处ebreak指令的执行会将当前Core给Halt住,同时切换当前模式为Debug模式,并等待OpenOCD的垂询.

第二个预备知识: 在OpenOCD启动之后,在它的启动Terminal窗口,会显示出3333/4444/5555三个端口,我们需要使用的是Telnet端口,也就是4444端口.如果读者不喜欢这个数字,其实是可以自行修改的(如下所示),修改完成后,重新编译生成OpenOCD即可.

riscv-openocd/src/server/telnet_server.c 文件中的 telnet_register_commands()中:
telnet_port = strdup("4444");

启动Telnet的命令步骤如下:

(1)新开启一个Terminal窗口
(2)在窗口中输入命令: telnet  localhost  4444 或者  telnet  127.0.0.1   4444

即可将telnet与OpenOCD建立起通信来,他们之间的通信连接方式也是socket的方式,同GDB与OpenOCD之间的方式一样的.

到这里预备知识就准备完毕了.

4. 在Telnet中输入命令完成flash的刷写

PROBE命令的分析:
首先,需要在Telnet的Terminal窗口中输入如下命令:

flash probe <bank-number>

此时如果FLASH设备上有对应的bank号,那么就会打印出对应的flash设备的信息. 由该命令的前两个字符串"flash probe", OpenOCD可以找到已注册的handler函数handle_flash_probe_command(). 该函数的调用栈如下图所示. 通过CMD_ARGV[0]中存放的bank-name从全局变量flash_banks[]中查找对应的flash_bank{}实例. 然后从预定义数组target_devices[]中找到对应的项,该项包含了重要的信息:在CPU-Core上SPI寄存器空间的基地址,后续需要访问CPU上的SPI总线寄存器都需要根据该基地址.

要从target_devices[]数组中找到对应的表项,主要是通过"tap idcode"来查找的,这个IDCODE就是存放在Debug-Module寄存器中的一个ID寄存器(在RISCV平台上,就是IDCODE 0x01寄存器), 存放在target_devices[]数组中的值要与该IDCODE寄存器中的值一致才可以被搜索到.下面是该数组内容的一个截图, 它每一行属于一项内容,每项又有三个element,我们刚才讲的是指第二个element的值要匹配.那么第三个element就是我们说的"ctrl_base",它就是SPI寄存器域的基地址,这个信息需要查询相应的CPU的手册才能获得到.
该表存放在riscv-openocd/src/flash/nor/fespi.c文件中.

在确定好了SPI的基地址寄存器后,可以看到后续又通过SPI总线访问到FLASH设备上的信息"flash-id".通过该ID信息与下图表(全局预配置表"flash_devices[]")中的device_id进行匹配. 该表的内容包含各种FLASH设备的信息,该表存放在riscv-openocd/src/flash/nor/spi.c文件中,现截图如下,大家可以观察它的注释,里面有清晰的对各项的解释.

通过以上的分析可以知道:如果工程师正确的配置了以上两个表,那么probe过程就会顺利完成.

WRITE-FLASH命令的分析

在完成第一步的probe后, 就可以开始进行写flash的操作了,它的命令如下:

flash write_image erase <your-image-file> <address> bin
or
flash write_bank <bank_id> <your-image-file> <offset>

我们就分析 "flash write_bank" 这条命令,另一条命令多了erase的操作,写的动作其实是一样的. 通过这条命令字串,OpenOCD可以很顺利的找到预注册的handler函数handle_flash_write_bank_command(),下图是该函数的调用栈截图, 这个函数稍长一些,下面会解释详细解释.

对于函数handle_flash_write_bank_command(),详细分析如下:
(1) 首先根据传递进来的bank_id通过查找flash_banks[]数组找到匹配项.
(2) 获取offset的值,该值是相对于bank的offset,未来作为一个目的地址使用的.
(3) 打开输入的image-file的句柄,并读取到本地新分配的buffer中
(4) 然后调用flash_driver_write()函数,开始将存放好的image-file中的数据写入到特定的flash-bank中.

对于flash_driver_write()->fespi_write()函数,详细分析如下:
(1) 校验flash要写入的bank中的每个sector是否被保护了,若是则无法继续写则返回
(2) 确定好后面要下载的片上运行小程序及其大小: riscv32_bin/riscv64_bin, 就是预备知识里面讲到的程序
(3) 调用target_alloc_working_area()函数: 先在OpenOCD中开辟一个buffer,大小是要下载的bin的大小.建立相应的管理结构体. 调用函数target_read_memory()将Core上由target->working_area_phys指定的地址,由target->working_area_size指定大小的这块区域的数据读取到本地刚开辟的buffer中,备份起来.
(4) 调用target_write_buffer()函数,将小程序bin(SPI的驱动代码)写入到target->working_area_phys指定的起始地址处.如果写的过程中发生失败,那么调用target_free_working_area()函数将备份的数据恢复到Core中去.
(5) 调用target_get_working_area_avail()函数中,将最终要写入的image-file那么大的一块数据这样的空间对应的一个管理实例给开辟出来.
(6) 再次调用target_alloc_working_area()函数,这次开辟的数据区是为了要下载image-file中的数据到FLASH中而分配的,因为先由OpenOCD下载数据到Core的working-area中,最后才有片上小程序将working-area中存放的image-file中的数据写入到FLASH中去的. 所以Core中的working-area上的这块区间的数据也需要备份到OpenOCD的本地buffer中.
(7) 在调用target_write_buffer()函数写入image-file中的数据之前,首先需要配置片上小程序在运行时的那六个入参(这个在预备知识中已经提及).
(8) 这里就开始调用target_write_buffer()将image-file中的数据写入到Core中由configue文件中指定的working-area中了.
(9) 调用target_run_algorithm()函数,同时把预先配置好的含有六个入参的结构发送到Core中去,写入到Core中的a0~a5这六个寄存器中,作为片上小程序在"jal flash_fespi"时的入参. 这六个参数包含了SPI寄存器域的基地址,要访问的image-file临时存放的物理地址以及它的大小.
(10) 当片上小程序完成将image-file的数据由working-area通过SPI总线写入到FLASH这个过程后,就会从flash_fespi()函数返回后,就会执行"ebreak"指令而Halt住.
(11) 其实target_write_buffer()以及target_run_algorithm()函数都是多次被调用的, OpenOCD写入一部分数据到Core中的DDR中,就会触发片上小程序将这一部分数据搬移写入到FLASH中,这也是为了避免如果image文件过大,尝试在一次搬移中就完成的话,那么写入的时间就会过长,那样可能会出现问题,所以不得不切割成多次来处理.
(12) 在每次运行target_run_algorithm()函数时,OpenOCD都会持续的polling当前Debug-Module的状态,如果polling到的状态是"RISCV_HALT_BREAKPOINT",那么就表示当前的这次传输完毕了.就可以调整参数进行下一次的传输了. (其实这部分我在<<RISCV上Semihosting功能浅析>>文章已经涉及到了)
(13) 完成所有的传输后,分别调用两次target_free_working_area()对备份到OpenOCD本地的两个数据区恢复到Core上对应的区间中去. 等于片上小程序以及image的缓存区的数据就被覆盖掉了.

校验写入的数据

完成上述的写入操作之后,就是最后一步校验的工作,它的命令如下:

flash verify_bank <bank_number> <your-image-file> <offset>

OpenOCD通过命令字串"flash verify_bank"可以找到预注册的handler函数handle_flash_verify_bank_command(). 它实际的操作很简单就是重新打开image-file,读出其中的数据,将其与从FLASH中读取的数据进行比较即可.

5. 总结:

以上就是对OpenOCD所支持的FLASH烧写逻辑的分析. 可以看到它主要采用了下载一个片上小程序到CPU上去的方式,由小程序来完成数据的搬移工作. OpenOCD先将要烧写的image写入到CPU中由cfg文件指定的working-area中,然后由小程序通过SPI总线写入到FLASH中,所有烧写工作完成后,毁尸灭迹不留一点痕迹!

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值