关于xilinx zynq petalinux 使用32MB(W25Q256)的SPI FLSH无法正常读写16M以上内存的BUG的调试过程
最近在玩zynq petalinux时发现了一个蛋疼的BUG,我的板子使用的是W25Q256芯片,32MB容量,被我分成了u-boot,env,kernel,rootfs几个mtd块,其中的rootfs是放在flash末尾,也就是超过了spiflash一半(16MB),当系统正常启动后,挂载rootfs时始终不成功,并且使用flash_erase /dev/mtd3 这种命令对rootfs区域进行擦除时竟然把flash前面部分的u-boot和kernel给擦掉了,心里一万个MLGB,又是xilinx官方驱动有坑了,没办法,只能去啃官方代码了
首先,分析现象,启动kernel正常,而且kernel有一部分也超过了16MB地址,因此u-boot应该没问题,问题出在kernel mtd或者qspi flash驱动内。w25q芯片默认使用的是3个字节地址寻址,3个字节最大只能16MB,因此要寻址超过16MB时需要增长寻址位数,在w25q256中增加寻址位数有两种方式,一种是直接切换为4直接寻址,有专门的命令切换,切换后读写命令都使用4直接地址。第二种是使用EAR寄存器,读写数据时还是使用3字节地址,但是通过EAR寄存器可以切换读写的是低16M还是高16M
经过一番艰苦的debug分析,最终定位到了内核mtd驱动层的xxxxx/linux-xlnx/drivers/mtd/spi-nor/spi-nor.c文件内,在int spi_nor_scan函数中,xilinx官方竟然强制把qspi驱动限制死了3字节地址模式,看不懂他这种骚操作是什么意思,怪不得我即使给W25q256 的属性里面添加了4字节地址支持的属性也无效
......
if (nor->addr_width) {
/* already configured from SFDP */
} else if (info->addr_width) {
nor->addr_width = info->addr_width;
} else if (mtd->size > 0x1000000) {
#ifdef CONFIG_OF //关键在这里,xilinx判断设备树种的qspi 描述
np_spi = of_get_next_parent(np);
if (of_property_match_string(np_spi, "compatible",
"xlnx,zynq-qspi-1.0") >= 0) {
int status;
nor->addr_width = 3; //强制使用了3字节地址模式
set_4byte(nor, info, 0);
status = read_ear(nor, info);
if (status < 0)
dev_warn(dev, "failed to read ear reg\n");
else
nor->curbank = status & EAR_SEGMENT_MASK;
} else {
#endif
/*
* enable 4-byte addressing
* if the device exceeds 16MiB
*/
nor->addr_width = 4;
if (JEDEC_MFR(info) == SNOR_MFR_SPANSION ||
info->flags & SPI_NOR_4B_OPCODES)
spi_nor_set_4byte_opcodes(nor, info)
......
后来想想,xilinx这样做应该是想通过ear寄存器来操作读写,不想直接切换成4字节地址,后面继续分析也验证了这个想法,看下面的代码就清楚了,只不过不知道xilinx是忘了改还是瞧不起WINBON,在判断是否操作EAR寄存器时漏掉了一个WINBON判断,导致我使用W25q256时这个判断就失效了,从而导致高16MB的flash读写出错,具体修改看下面的代码注释就知道了
static int spi_nor_write_ear(struct spi_nor *nor, u32 ear)
{
u8 code = SPINOR_OP_WREAR;
u8 addr;
int ret;
struct mtd_info *mtd = &nor->mtd;
if (mtd->size <= (0x1000000) << nor->shift)
return 0;
ear = ear % (u32)mtd->size;
addr = ear >> 24;
if (!nor->isstacked && addr == nor->curbank)
return 0;
if (nor->isstacked && mtd->size <= 0x2000000)
return 0;
if (nor->jedec_id == CFI_MFR_AMD)
code = SPINOR_OP_BRWR;
if (nor->jedec_id == CFI_MFR_ST ||
nor->jedec_id == CFI_MFR_MACRONIX ||
nor->jedec_id == SNOR_MFR_ISSI ||
nor->jedec_id == SNOR_MFR_WINBOND) { //WINBON是我加的,原本是没有的
write_enable(nor);
code = SPINOR_OP_WREAR;
}
nor->bouncebuf[0] = addr;
if (nor->spimem) {
struct spi_mem_op op =
SPI_MEM_OP(SPI_MEM_OP_CMD(code, 1),
SPI_MEM_OP_NO_ADDR,
SPI_MEM_OP_NO_DUMMY,
SPI_MEM_OP_DATA_OUT(1, nor->bouncebuf, 1));
ret = spi_mem_exec_op(nor->spimem, &op);
} else {
ret = nor->write_reg(nor, code, nor->bouncebuf, 1);
}
nor->curbank = addr;
return ret;
}
static int read_ear(struct spi_nor *nor, struct flash_info *info)
{
int ret;
u8 code;
/* This is actually Spansion */
if (JEDEC_MFR(info) == CFI_MFR_AMD)
code = SPINOR_OP_BRRD;
/* This is actually Micron */
else if (JEDEC_MFR(info) == CFI_MFR_ST ||
JEDEC_MFR(info) == CFI_MFR_MACRONIX ||
JEDEC_MFR(info) == SNOR_MFR_ISSI ||
JEDEC_MFR(info) == SNOR_MFR_WINBOND) //WINBON是我加的,原本是没有的
code = SPINOR_OP_RDEAR;
else
return -EINVAL;
if (nor->spimem) {
struct spi_mem_op op =
SPI_MEM_OP(SPI_MEM_OP_CMD(code, 1),
SPI_MEM_OP_NO_ADDR,
SPI_MEM_OP_NO_DUMMY,
SPI_MEM_OP_DATA_IN(1, nor->bouncebuf, 1));
ret = spi_mem_exec_op(nor->spimem, &op);
} else {
ret = nor->read_reg(nor, code, nor->bouncebuf, 1);
}
if (ret < 0) {
pr_err("error %d reading EAR\n", ret);
return ret;
}
return nor->bouncebuf[0];
}
按照上面的代码注释添加WINBON的判断后重新编译下载启动都正常了,后面查看了petalinux2020.2的代码,发现这个bug还是存在,难道就没人在zynq上用超过16M的spiflah?