用来干什么
手上有两个路由器,一个小米的r3gv2(已砖,BootLoader莫得了),一个斐讯K3(已砖,nand闪存内的BootLoader也莫得了),对于小米的路由器,把spi flash焊下来,用编程器刷入breed就可以了。而对于斐讯K3来说,PCB上也预留了SPI flash的焊盘,可以改SPI启动,再向nand闪存中刷入CFE(BootLoader的一种)就可以。
无奈的是,因为疫情原因,买编程器都不发货,所以苦思冥想,终于想到可以使用带有SPI功能的可编程设备就可以做一个编程器。至于如何向spi中写入文件,不是有sscom嘛,可以直接通过串口发送文件,然后在Arduino中做个串口接收SPI写入就可以了。
SPI Flash
SPI flash的品牌有很多,GD或者winbond,大多数都是8PIN的,在路由器,机顶盒中随处可见,小米路由器用的128Mbit(16M)的。以winbond为例,命名为W25QXX,图片如下:
不同品牌的这种芯片在引脚定义和读取命令上几乎是兼容的。在Arduino Mega 2560中,SPI引脚默认是50,51,52,53这四个管脚。
- DI----MOSI(51)
- DO----MISO(50)
- CS----SS(53)
- CLK—SLK(52)
- VCC,WP/,HOLD/----3.3V
SPI 的读写
spi flash的一个基本的特点就是,在写入之前必须擦除,擦除的地方都是0xFF,我们在写入SPI Flash的过程主要用到的就是读取ID(校验程序是否正确)、芯片擦除、页面编程(写入数据)和数据读取(数据校验)三个命令。
程序
功能函数
#include <SPI.h>
#define WB_WRITE_ENABLE 0x06
#define WB_WRITE_DISABLE 0x04
#define WB_CHIP_ERASE 0xc7
#define WB_READ_STATUS_REG_1 0x05
#define WB_READ_DATA 0x03
#define WB_PAGE_PROGRAM 0x02
#define WB_JEDEC_ID 0x9f
//打印256字节的数据(16x16)
void print_page_bytes(byte* page_buffer) {
char buf[10];
for (int i = 0; i < 16; ++i) {
for (int j = 0; j < 16; ++j) {
sprintf(buf, "%02x", page_buffer[i * 16 + j]);
Serial.print(buf);
}
Serial.println();
}
}
void get_jedec_id(void) {
Serial.println("command: get_jedec_id");
byte b1, b2, b3;
_get_jedec_id(&b1, &b2, &b3);
char buf[128];
sprintf(buf, "Manufacturer ID: %02xh\nMemory Type ID: %02xh\nCapacity ID: %02xh",
b1, b2, b3);
Serial.println(buf);
Serial.println("Ready");
}
void chip_erase(void) {
Serial.println("command: chip_erase");
_chip_erase();
Serial.println("Ready");
}
void read_page(unsigned int page_number) {
char buf[80];
sprintf(buf, "command: read_page(%04xh)", page_number);
Serial.println(buf);
byte page_buffer[256];
_read_page(page_number, page_buffer);
print_page_bytes(page_buffer);
Serial.println("Ready");
}
void read_all_pages(void) {
Serial.println("command: read_all_pages");
byte page_buffer[256];
for (int i = 0; i < 4096; ++i) {
_read_page(i, page_buffer);
print_page_bytes(page_buffer);
}
Serial.println("Ready");
}
void _get_jedec_id(byte* b1, byte* b2, byte* b3) {
digitalWrite(SS, LOW);
SPI.transfer(WB_JEDEC_ID);
*b1 = SPI.transfer(0xFF); // manufacturer id
*b2 = SPI.transfer(0xFF); // memory type
*b3 = SPI.transfer(0xFF); // capacity
digitalWrite(SS, HIGH);
}
void _chip_erase(void) {
digitalWrite(SS, LOW);
SPI.transfer(WB_WRITE_ENABLE);
digitalWrite(SS, HIGH);
delay(10);
digitalWrite(SS, LOW);
SPI.transfer(WB_CHIP_ERASE);
digitalWrite(SS, HIGH);
delay(10);
digitalWrite(SS, LOW);
SPI.transfer(WB_WRITE_DISABLE);
digitalWrite(SS, HIGH);
not_busy();
}
void _read_page(word page_number, byte* page_buffer) {
digitalWrite(SS, LOW);
SPI.transfer(WB_READ_DATA);
SPI.transfer((page_number >> 8) & 0xFF);
SPI.transfer((page_number >> 0) & 0xFF);
SPI.transfer(0);
for (int i = 0; i < 256; ++i) {
page_buffer[i] = SPI.transfer(0xFF);
}
digitalWrite(SS, HIGH);
not_busy();
}
void _write_page(word page_number, byte* page_buffer) {
digitalWrite(SS, LOW);
SPI.transfer(WB_WRITE_ENABLE);
digitalWrite(SS, HIGH);
delay(10);
digitalWrite(SS, LOW);
SPI.transfer(WB_PAGE_PROGRAM);
SPI.transfer((page_number >> 8) & 0xFF);
SPI.transfer((page_number >> 0) & 0xFF);
SPI.transfer(0);
for (int i = 0; i < 256; ++i) {
SPI.transfer(page_buffer[i]);
}
digitalWrite(SS, HIGH);
digitalWrite(SS, LOW);
SPI.transfer(WB_WRITE_DISABLE);
digitalWrite(SS, HIGH);
not_busy();
}
uint8_t read_status(void)
{
digitalWrite(SS, LOW);
SPI.transfer(WB_READ_STATUS_REG_1);
uint8_t data = SPI.transfer(0xFF);
digitalWrite(SS, HIGH);
return data;
}
void not_busy(void) {
while (read_status() & 0x01)
{
delay(1);
}
}
串口转发数据到SPI
每次读取一个字节,读满256个字节后,通过spi写入flash,通过指定文件大小,判断是否结束,不满一页的数据剩下的都是0xFF。因为有些BootLoader的文件末尾都是0xFF,所以可以指定文件大小来提前结束写入过程,节省时间,要写入的二进制文件可以通过winhex查看。
在写入完一页后,读出这一页,和写入数据做校验,如果有错误,程序return,停止运行。
因为是一个字节一个字节的从串口读,所以串口的波特率不要太大,否则会丢数据,9600bps比较合适。可以在调试时,可以吧FILE_SIZE改为256,把打印页面数据的注释去掉。
没问题后,通过sscom发送文件的功能,在擦除芯片几秒后,点击发送文件,串口会输出当前写入的页面序号。
#define FILE_SIZE 524288 //512KB
void setup()
{
SPI.begin();
SPI.setDataMode(0);
SPI.setBitOrder(MSBFIRST);
digitalWrite(SS, HIGH);
Serial.begin(9600);
Serial.println("Ready");
init_printf();
not_busy();
get_jedec_id();
chip_erase();
while (Serial.read() > 0);
}
void loop()
{
if (!writed && Serial.available() > 0)
{
bytes++;
page[currentPos] = Serial.read();
currentPos++;
if (currentPos == 256)
{
Serial.println("Receive Page:" + String(currentPage));
//print_page_bytes(page);
_write_page(currentPage, page);
delay(10);
byte page_temp[256];
_read_page(currentPage, page_temp);
//Serial.println("Read Page:" + String(currentPage));
//print_page_bytes(page_temp);
for (int k = 0; k < 256; k++)
{
if (page_temp[k] != page[k])
{
Serial.println("Error!");
return;
}
}
if (bytes >= FILE_SIZE)
{
writed = true;
Serial.println("WRITE FINISH!!!");
}
memset(page, 0xFF, 256 * sizeof(byte));
currentPos = 0;
currentPage++;
}
else if (bytes >= FILE_SIZE)
{
Serial.println("Receive Page:" + String(currentPage));
//print_page_bytes(page);
_write_page(currentPage, page);
delay(10);
byte page_temp[256];
_read_page(currentPage, page_temp);
//Serial.println("Read Page:" + String(currentPage));
//print_page_bytes(page_temp);
for (int k = 0; k < 256; k++)
{
if (page_temp[k] != page[k])
{
Serial.println("Error!");
return;
}
}
writed = true;
Serial.println("WRITE FINISH!!!");
}
}
if (writed)
{
Serial.read();
}
}
如果遇到写入数据不正确,随机错误、重复错误或者很多0x00的情况,八成是电压的问题,我用3.3V的电压,操作GD的Flash就没有问题,写winbond的Flash就写不进去,读出来都是乱的,然后在Flash擦除之后,断了一下芯片的供电,再写入就好了。