自研MCU芯片闪存驱动的实现:OpenOCD详细过程记录与操作指南
在嵌入式系统开发领域,MCU(微控制单元)芯片在众多智能设备中发挥着核心的控制作用,其性能优化与功能拓展一直是技术发展的重要方向。OpenOCD(Open On-Chip Debugger)作为一个功能强大的开源调试工具,广泛应用于嵌入式系统开发中,为系统调试与程序烧录提供了重要支持。
随着自研MCU芯片项目的不断推进,如何实现其与OpenOCD的无缝对接成为关键问题之一。而闪存驱动作为连接MCU与外部存储、实现数据高效存储与读取的核心组件,其与OpenOCD的适配对于提升整个系统开发效率具有重要意义。
本文档系统性地记录了国科安芯自研MCU芯片AS32A601添加闪存驱动至OpenOCD工具的完整过程。通过深入研究OpenOCD的架构与工作原理,结合芯片的技术手册,详细阐述了驱动编写、调试与验证的各个步骤。旨在为后续类似项目开发提供详实的参考与借鉴,助力嵌入式开发人员高效实现MCU芯片与OpenOCD的适配,加速产品研发与技术迭代进程。
OpenOCD编译环境搭建
注:本节所有内容是基于Windows系统开发,如使用其他操作系统请自行参考。
安装MSYS2
-
安装MSYS2需要进入到MSYS2官网下载安装包,进入官网后如图点击msys2-x86_64-20240727.exe链接下载安装包。
安装步骤:
-
双击运行安装程序,出现如图所示信息单击下一步:
-
选择软件安装路径后单击下一步:
-
出现如图界面单击下一步:
-
等待安装(可能比较慢请耐心等待):
-
如果在安装过程中出现卡在50%一直不动等情况,可以单击取消
添加环境
-
打开我的电脑,进到安装目录找到mingw64文件夹,点击进去找到bin文件夹点击进去复制路径。
-
设置环境变量:将上面复制的路径添加到环境变量中。
-
更换镜像源:更换国内镜像源,此处参考阿里镜像源上关于msys2的方式进行更换
-
编辑 /etc/pacman.d/mirrorlist.mingw32 ,在文件开头添加:Server = https://mirrors.aliyun.com/msys2/mingw/i686
-
编辑 /etc/pacman.d/mirrorlist.mingw64 ,在文件开头添加:Server = https://mirrors.aliyun.com/msys2/mingw/x86_64
-
编辑 /etc/pacman.d/mirrorlist.msys ,在文件开头添加:Server = https://mirrors.aliyun.com/msys2/msys/$arch
-
编辑 /etc/pacman.d/mirrorlist.ucrt64 ,在文件开头添加:Server = https://mirrors.aliyun.com/msys2/mingw/ucrt64
安装依赖环境
点击相应的命令行软件打开命令行窗口,这里选择“MSYS2 MINGW64”。
-
更新MSYS2,使用以下指令更新系统文件。直至无更新内容为止。
-
安装依赖环境 不同的操作系统对安装的环境有些许的差异要求,一般而言不同的软件依赖包有差异的一般为64bit的标识为“x86_64”,而32bit系统一般为“i686”。
-
64bit操作命令(使用时直接复制粘贴即可):
-
32bit操作命令(使用时直接复制粘贴即可):
OpenOCD源码获取和编译流程
下载OpenOCD
-
打开MSYS2 MINGW64命令行窗口,跳转到要克隆的地址,使用 git clone 命令将最新的OpenOCD源码克隆至本地。(此处给出一个git clone命令操作示例,如需下载其他版本的源码,请自行修改克隆地址) git clone https://github.com/riscv-collab/riscv-openocd.git
注:后续所有输入指令操作,都是在MSYS2 MINGW64命令行窗口中操作,需要将路径跳转到克隆源码地址下,如图所示。
-
在MSYS2 MINGW64命令行窗口,使用cd命令将路径跳转到刚克隆的源码地址下,输入./bootstrap 命令克隆OpenOCD的依赖文件,如下图所示。
配置OpenOCD
-
使用以下指令配置OpenOCD,这里选择“--enable-jlink”配置,其他配置(只要安装了相应的依赖)会默认开启:
-
配置过程需要一些依赖包,如果配置过程中提示依赖包版本不足或没有安装,需要先安装依赖包。 根据提示,使用pacman -Ss + 搜索依赖包名,即可找到依赖包。 使用pacman -S + 安装依赖包名,即可完成依赖包的安装。
-
安装依赖包完成后,继续执行第一步操作,直至配置完成。
OpenOCD添加驱动
文件总览
以AS32I601为例,在整个自研芯片集成过程中,需要编写并测试如下设备支持文件:
-
闪存驱动程序文件(as32i601.c)
-
内置闪存驱动程序列表文件(drivers.c)
-
编译脚本文件(Makefile.am)
-
运行脚本文件(as32i601.cfg)
目前,文件实现了编程功能,其他功能可后续补充,不影响实际功能使用。
设备支持文件存放结构如下:
-
\openocd\src\flash\nor\as32i601.c
-
\openocd\src\flash\nor\drivers.c
-
\openocd\src\flash\nor\Makefile.am
-
\openocd\tcl\target\as32i601.cfg
文件详解
闪存驱动程序文件(as32i601.c)
由于本芯片特性,烧录过程无法通过RAM运行片上代码的形式进行代码烧录,因此选用了OpenOCD直接操作qspi寄存器的形式完成flash代码烧录,根据文件中flash_driver结构体所示,该文件需要完成flash_bank_command、erase、write、probe函数编写,其他函数可采用官方库文件函数和参考其他芯片驱动文件即可。
flash_driver结构体,该结构体定义了OpenOCD闪存驱动程序所需的所有回调。
conststructflash_driveras32i601_flash = { .name = "as32i601", .commands = as32i601_command_handlers, .flash_bank_command = as32i601_flash_bank_command, .erase = as32i601_erase, .protect = as32i601_protect, .write = as32i601_write, .read = default_flash_read, .probe = as32i601_probe, .auto_probe = as32i601_probe, .erase_check = default_flash_blank_check, .info = get_as32i601_info, .free_driver_priv = default_flash_free_driver_priv, };
第一行,as32i601_flash为闪存驱动程序名称,这个名称需要放到内置闪存驱动程序列表文件(drivers.c)中。 第二行,as32i601为定义的闪存名字,此名称会在运行脚本文件(as32i601.cfg)中用到。 第四行,flash_bank_command为通过脚本文件传入的信息,用于获取芯片信息,如芯片地址等。 第五行,erase为擦除函数,该函数传入擦除的起始块和终止块,需要在此函数中实现擦除功能。 第六行,write为写入函数,该函数传入写入的起始地址、写入的数据和写入的大小,需要在此函数中实现写入功能。
flash_bank_command实现函数为as32i601_flash_bank_command,可以在此函数中获取闪存库命令。
FLASH_BANK_COMMAND_HANDLER(as32i601_flash_bank_command) { if (CMD_ARGC < 7) return ERROR_COMMAND_SYNTAX_ERROR; bank->driver_priv = malloc(sizeof(as32i601_priv)); COMMAND_PARSE_NUMBER(u32, CMD_ARGV[6], priv->ctrlAddress); LOG_INFO("as32i601 probe: 0x%x ctrlAddress", priv->ctrlAddress); priv->probed = 0; return ERROR_OK; }
第六行,为as32i601_priv结构体申请一段内存。 第七行,通过命令行获取控制器基地址,并赋值给priv->ctrlAddress。
erase实现函数为as32i601_erase,此函数中实现擦除功能。
intas32i601_erase(struct flash_bank *bank, unsignedint first, unsignedint last) { structtarget *target = bank->target; if (target->state != TARGET_HALTED) { LOG_ERROR("Target not halted"); return ERROR_TARGET_NOT_HALTED; } LOG_INFO("as32i601_erase"); // flash memory mass erase as32i601_flashStop(bank); for(unsignedint sector = first; sector <= last; sector += 0x400) { as32i601_Write_Reg(bank, QSPI_CR, 0x00000031); as32i601_Write_Reg(bank, QSPI_CCR, 0x00000106); as32i601_WaitNotBusy(bank); as32i601_Write_Reg(bank, QSPI_CCR, 0x000025d8); as32i601_Write_Reg(bank, QSPI_AR, bank->sectors[sector].offset); as32i601_WaitNotBusy(bank); as32i601_read_flashstatus(bank); bank->sectors[sector].is_erased = 1; } return ERROR_OK; }
第十五行,0x400为实际芯片扇区大小除以行大小所得到的值。因为此版本的OpenOCD会将bin文件大小按照所设置的扇区大小对齐,所以此文件将扇区参数设置为行大小。 第十七到二十四行为扇区擦除操作,这里不过多进行讲解,as32i601_Write_Reg函数是通过OpenOCD底层函数target_write_memory封装而来,可以根据自己需求进行自定义封装。
write实现函数为as32i601_write,此函数中实现bin文件写入功能。
staticintas32i601_write(struct flash_bank *bank, constuint8_t *buffer,uint32_t offset, uint32_t count) { structtarget *target = bank->target; if(target->state != TARGET_HALTED){ LOG_ERROR("Target not halted"); return ERROR_TARGET_NOT_HALTED; } if(offset & 0x03){ LOG_ERROR("offset 0x%" PRIx32 " breaks required 4-byte alignment", offset); return ERROR_FLASH_DST_BREAKS_ALIGNMENT; } if(count & 0x3){ LOG_ERROR("size 0x%" PRIx32 " breaks required 4-byte alignment", count); return ERROR_FLASH_DST_BREAKS_ALIGNMENT; } as32i601_flashStop(bank); uint32_t addr = offset; LOG_INFO("as32i601 probe: %d count, 0%d addr", count, addr); while(count >= 256) { as32i601_Write_Reg(bank, QSPI_CR, 0x00000031); as32i601_Write_Reg(bank, QSPI_CCR, 0x00000106); as32i601_WaitNotBusy(bank); as32i601_Write_Reg(bank, QSPI_CCR, 0x00802502); as32i601_Write_Reg(bank, QSPI_AR, addr); as32i601_Write_Reg(bank, QSPI_DLR, 256-1); // flash memory word programfor(uint32_t address = 0; address < 256; address += 4) { constuint8_t *t_buffer = buffer + address; uint32_t value = buf_get_u32(t_buffer, 0, 32); as32i601_Write_Reg(bank, QSPI_DR, value); } buffer += 256; as32i601_WaitNotBusy(bank); as32i601_read_flashstatus(bank); LOG_INFO("as32i601 probe: %d count", count); addr = addr + 0x100; count = count - 0x100; } LOG_INFO("as32i601 flash write success"); return ERROR_OK; }
第五行到第十六行,为判断当时cpu状态和传入参数是否正确。 第三十八行,为将八位数组转换为32位数据,此函数为OpenOCD底层函数。 第二十三行到第四十九行,为写入操作,这里不过多进行讲解。
probe实现函数为as32i601_probe,通过此函数设定flash信息。
staticintas32i601_probe(struct flash_bank *bank) { LOG_INFO("as32i601 qspi_probe"); if(!priv->probed){ bank->num_sectors = bank->size/(256); uint32_t offset = 0; bank->sectors = malloc(sizeof(struct flash_sector) * bank->num_sectors); for (unsignedint i = 0; i < bank->num_sectors; i++) { bank->sectors[i].offset = offset; bank->sectors[i].size = 256; offset += bank->sectors[i].size; bank->sectors[i].is_erased = -1; bank->sectors[i].is_protected = 1; } priv->probed = 1; } return ERROR_OK; }
第六行,通过bin文件大小计算出所用扇区数量,因为此版本的OpenOCD会将bin文件大小按照所设置的扇区大小对齐,所以此文件将扇区参数设置为行大小。 第九行到第十五行,初始化扇区信息。
内置闪存驱动程序列表文件(drivers.c)
此文件列表中定义了所有闪存,需要将芯片信息加入其中。 (注:不同版本的OpenOCD此处可能略有差异,需要根据实际情况进行修改)
externconststructflash_driveras32i601_flash;staticconststructflash_driver * constflash_drivers[] = { &aduc702x_flash, &aducm360_flash, ... &as32i601_flash, ... &xmc4xxx_flash, &w600_flash, &rsl10_flash, NULL, };
第一行和第七行,根据格式添加信息,此处为as32i601_flash。
编译脚本文件(Makefile.am)
此文件为编译脚本文件,添加驱动文件路径。
NOR_DRIVERS = \ %D%/aduc702x.c \ %D%/aducm360.c \ ... %D%/str9xpec.c \ %D%/swm050.c \ %D%/as32i601.c \ ... %D%/xmc1xxx.c \ %D%/xmc4xxx.c
第七行,根据格式添加闪存驱动程序文件(as32i601.c)。
运行脚本文件(as32i601.cfg)
此文件为运行脚本文件,包含芯片配置信息。
adapter driver jlink adapter speed 1000 transport select jtag reset_config srst_nogate set _CHIPNAME riscv jtag newtap $_CHIPNAME cpu -irlen 5 -expected-id 0x10002FFFset _TARGETNAME $_CHIPNAME.cpu target create $_TARGETNAME.0 riscv -chain-position $_TARGETNAME $_TARGETNAME.0 configure -work-area-phys 0x20000000 -work-area-size 0x80000 -work-area-backup 1set _FLASHNAME $_CHIPNAME.flash flash bank $_FLASHNAME as32i601 0x100000000x0100000000 $_TARGETNAME.00x42100800 init halt
第一行,为适配器驱动,此处为jlink。 第三行,为适配器传输速度,此处为1000。 第五行,为传输方式,此处为jtag。 第七行,为复位配置。 第九行,为芯片类型,此处为riscv。 第十行,设置IR寄存器长度和ID号。 第十五行,设置sram的物理地址和长度(以自定义芯片为例)。 第十八行,设置闪存名称(此名称和闪存驱动程序文件中的name名称对应)、闪存地址、大小和控制寄存器基地址(以自定义芯片为例)。
编译OpenOCD
-
使用如下指令编译OpenOCD
注:若编译过程中出现如图所示错误,说明calloc函数所使用的头文件与实际代码使用版本不匹配,需要将头文件修改为正确的版本。
-
编译错误失败原因分析:void *__cdecl calloc(size_t _NumOfElements,size_t _SizeOfElements);
-
本地库环境修改
-
mingw下载
-
打开MinGW官网,点击此处下载安装包
-
双击进行软件安装
-
等待安装完成
-
编译器安装
-
勾选“mingw32-gcc”
-
点击“Apply Changes”
-
点击“Apply”
-
等待更新完成
-
添加环境变量 打开系统环境变量设置窗口,如图所示,将安装路径“C:\MinGW\bin和C:\MinGW\include”填入环境变量。(若安装在其他路径,可自行修改添加)
-
编译完成后,为后续使用方便,可以将相应文件复制出来完成对OpenOCD的打包压缩。
闪存驱动使用流程与验证
注:本章所有输入指令操作,都是在cmd窗口中进行。
闪存下载
打开cmd窗口,使用如下命令,将bin文件下载到芯片中。(注:文件路径可使用绝对路径也可使用相对路径,在相应环境中进行修改即可) ./openocd/bin/openocd.exe -f ./openocd/target/as32i601.cfg -c "program ./SV32C601_Software/demo/gpio/GPIO_IO_test/OBJ/bin/gpio_io_test.bin 0x10000000" -c "shutdown" 命令执行后,会弹出一个窗口,等待窗口自动执行完成后结束。(此处包含了运行调试信息,若无需此信息,可自行在闪存驱动程序文件将对应代码注释即可)
运行界面如图所示:
闪存读取
打开cmd窗口,使用如下命令,连接OpenOCD并读取闪存,判断是否下载正确。
./openocd/bin/openocd.exe -f ./openocd/target/as32i601.cfg telnet localhost 4444 halt mdw 0x1000000040
第一行:在cmd窗口输入此命令,连接OpenOCD,OpenOCD会监听4444端口。 第三行:在一个新的窗口输入此命令,会连接telnet端口。 第五行:在telnet窗口输入此命令,操作为停止cpu。 第七行:在telnet窗口输入此命令,读取0x10000000地址的数据,读取宽度为40个字。
将读取的数据和bin文件进行对比,如果一致,则说明驱动程序执行正确,二进制文件下载成功。(注:此处需要多读取几个地址,以确保整个bin文件下载正确)
运行界面如图所示: OpenOCD连接界面:
读取内存界面: