一、简介

在使用STM32等单片机驱动显示屏时,为了显示中文字体,常用FLASH保存字库信息。但是字库的更新通常只能使用SD卡更新,在一些小型单片机系统(如STM32F103C8T6、STC89C52)上,没有增加SD卡支持的必要。为解决此问题,使用此项目,可以通过串口对FLASH的数据进行更新。

此方案包括如下部分功能:

  1. 读取FLASH芯片ID。可用于检测通信是否正常、FLASH是否正常工作。
  2. 擦除。可以整片擦除,可以指定地址和长度擦除。
  3. 生成字库文件。指定各字体字库文件,生成对应的数据头,并且拼接成完整的bin文件。
  4. 写入字库到FLASH。将所需要写入的字库bin文件写入到FLASH指定位置。
  5. 读取FLASH信息。将FLASH指定位置和长度的数据读出,从而用来检测字库是否写入正确。

此方案包括两个部分:

  1. 上位机部分。使用C#编程,软件为Microsoft Visual Studio Professional 2019。
  2. 下位机部分。硬件环境使用的是自制的Air32板子,主控为为Air32F103CBT6。软件使用C编程,硬件环境软件为MDK5.25版本。

如下为上位机软件界面和硬件实物图:
在这里插入图片描述
在这里插入图片描述
硬件实物中,目前只使用了2个串口以及FLASH芯片,其他未使用。

二、通信协议

通信方式上位机作为主控端,用户点击界面后下发命令到下位机,下位机根据命令执行响应的动作,生成响应数据包返回给上位机。

数据包格式:

HEADCMDLENDATACRC16END
长度(字节)222x22
内容特定,AA BB命令数据包总长度数据校验码特定,CC DD

命令和响应数据包都遵循此格式,区别在于数据包格式中的数据有差异,根据命令进行区分。

2.1 CMD

CMD为指定的命令,执行不同的功能,主要包括如下:

命令数值作用
CMD_GETINFO0x0000获取FLASH的信息
CMD_ERASE_CHIP0x0001整片擦除
CMD_ERASE0x0002指定地址、长度擦除
CMD_SAVE0x0003保存数据
CMD_READ0x0004读取数据

2.2 LEN

LEN为数据包总长度,为整包数据的总长度,包括数据长度 + 10字节(2字节HEAD、2字节CMD、2字节LEN、2字节CRC16、2字节END)。

2.3 DATA

DATA为命令对应的具体数据,目前分为如下四类:

  1. CMD_GETINFO:获取FLASH的信息。

    命令数据包:无DATA部分。

    响应数据包:返回FLASH的芯片ID和容量。

    芯片ID芯片容量
    长度(字节)44
  2. CMD_ERASE_CHIP:整片擦除。

    命令数据包:无DATA部分。

    响应数据包:返回当前擦除操作的起始地址(即0x0000 0000)和状态。

    地址状态
    长度(字节)42
  3. CMD_ERASE:擦除指定地址、指定长度。

    命令数据包:需要擦除的地址和长度。

    地址长度
    长度(字节)44

    响应数据包:返回当前擦除操作的起始地址(即0x0000 0000)和状态。

    地址状态
    长度(字节)42
  4. CMD_SAVE:将数据保存到FLASH指定地址、长度。

    地址长度数据
    长度(字节)44x

    响应数据包:指定地址的数据保存操作,状态表示当前操作是否成功,0表示成功,1表示失败。

    地址状态
    长度(字节)42
  5. CMD_READ:从FLASH读取指定地址、长度的数据。

    地址长度
    长度(字节)44

    响应数据包:返回需要读取的FLASH数据,不再包含地址等信息。

    数据
    长度(字节)x

注意:此部分只介绍了命令和响应数据包的DATA部分,其他部分(头和尾)每个命令都应包含,且均采用统一的通信协议。

2.4 CRC16

CRC16为DATA数据的校验码,使用的是Modbus CRC16校验。

上位机相关代码逻辑为:

//CRC16校验
public static UInt16 CalculateCRC16(byte[] data, UInt32 size)
{UInt16 wCrc = 0xFFFF;for (int i = 0; i < size; i++){wCrc ^= Convert.ToUInt16(data[i]);for (int j = 0; j < 8; j++){if ((wCrc & 0x0001) == 1){wCrc >>= 1;wCrc ^= 0xA001;//异或多项式}else{wCrc >>= 1;}}}return wCrc;
}

下位机相关代码逻辑为:

// 计算 Modbus CRC16 校验
static uint16_t calculateCRC16(const uint8_t *data, size_t length)
{uint16_t crc = 0xFFFF;for (size_t i = 0; i < length; ++i) {crc ^= data[i];for (int j = 0; j < 8; ++j) {if (crc & 0x0001) {crc = (crc >> 1) ^ 0xA001;} else {crc >>= 1;}}}return crc;
}

2.5 示例

  1. 获取信息:

    命令:BB AA 00 00 14 00 00 00 17 EF 00 00 00 00 00 01 DF EF DD CC

    响应:BB AA 00 00 0C 00 00 00 FF FF DD CC

  2. 整片擦除:

    命令:BB AA 01 00 0C 00 00 00 FF FF DD CC

    响应:BB AA 01 00 14 00 00 00 00 00 C0 00 00 00 00 00 51 0B DD CC

  3. 指定擦除:

    命令:BB AA 02 00 14 00 00 00 00 00 C0 00 29 6B 31 00 3D 1B DD CC

    响应:BB AA 02 00 14 00 00 00 00 00 C0 00 00 00 00 00 51 0B DD CC

  4. 保存数据:

    命令:保存的数据为00H - 3FH 共64(0x40)个数据。

    ​ BB AA 03 00 54 00 00 00 00 00 C0 00 40 00 00 00
    ​ 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
    ​ 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F
    ​ 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F
    ​ 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F
    ​ 82 E6 DD CC

    响应:BB AA 03 00 14 00 00 00 00 00 C0 00 00 00 00 00 51 0B DD CC

  5. 读取数据:

    命令:BB AA 04 00 14 00 00 00 00 00 C0 00 40 00 00 00 44 CB DD CC

    响应:BB AA 04 00 4C 00 00 00
    00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
    10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F
    20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F
    30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F
    D9 08 DD CC

注:因ARM与Visual Studio的存储模式均为小端模式,所以0xAABB在内存中的排列为BB AA。

三、字库格式

2.1 字库格式

字库文件主要包括如下部分:

内容长度(字节)描述
字库头1 + 字库数量*8保存字库标志、各字库的地址和长度
其长度根据字库数量自定义,本例中为1+4*8=33
字库1x字库1的完整数据
字库2x字库2的完整数据
字库3x字库3的完整数据
字库4x字库4的完整数据

3.2 字库头

长度(字节)描述
fontok1字库存在标志
0xAA,字库正常;其他,字库不存在
font1_addr4字库1的保存地址
font1_size4字库1的长度
font2_addr4字库2的保存地址
font2_size4字库2的长度
font3_addr4字库3的保存地址
font3_size4字库3的长度
font4_addr4字库4的保存地址
font4_size4字库4的长度

本例中字库数量为4个,所以字库头的长度为33字节。

四、上位机

4.1 界面介绍

在这里插入图片描述

界面主要按钮:

  1. 读取芯片信息:调用GETINFO命令,读取FLASH芯片的ID和容量信息,并回显到相关文本框。

  2. 整片擦除:调用EARSE_CHIP命令,将FLASH的所有数据进行擦除。相较于EARSE命令,执行较快。

  3. 擦除数据:调用EARSE命令,擦除指定地址和长度的数据,MCU执行此任务需要逐地址擦除,执行较慢。

  4. 生成数据:根据字库起始地址、各字库文件的长度生成字库头,并将字库头、各字库文件组合成二进制save.bin文件,保存成文件。

  5. 发送数据:将第4步生成的文件或本地保存的文件,调用SAVE命令发送到下位机端,保存到FLASH中。

    若单次发送数据的长度大于4096字节,会进行分割,每次发送4096字节,并且将当前的发送进度显示到进度条。

    发送完成后有弹窗提示,已发送的数据长度和总长度等信息。

  6. 读取文件:调用READ命令,读取指定地址、长度的数据,并且将数据保存到指定文件read.bin中。

    若单次读数据的长度大于4096字节,会进行分割,每次读取4096字节,并且将当前的读取进度显示到进度条。

    读取完成后有弹窗提示,已读取的数据长度和总长度等信息。

4.2 下位机通信流程

所有需要和下位机通信的操作均使用如下流程完成:

在这里插入图片描述

创建线程是由于部分操作(如读取、保存)花费时间较长,需要一直等待通信完成,会导致界面卡顿。

在进入通信任务后,先根据通信协议生成数据包,通过串口发送出去,再读取串口返回的响应数据包,依据通信协议判断响应数据包是否成功,将相应的数据执行对应的操作(读取的数据或者获取到的信息需要显示到文本框等)。如果当前操作已完成,则退出,如果未完成则继续发送、接收、处理的操作。

4.3 串口通信函数

在本部分将介绍上位机和上位机通信的两个主要通用函数——发送数据和读取数据,基于第二章中的通信协议,将所有的命令封装成此两个接口,完成和串口通信的任务,方便扩展更多的命令。

4.3.1 发送数据

//通用发送数据函数发送命令,前面添加头,后面添加尾,包含CRC16校验功能
//参数:发送数据cmd、数据
private bool SendData(CMD cmd, byte[] data)
{Header header = new Header();                                           //帧头end_t end = new end_t();                                                    //帧尾UInt16 len = (UInt16)(Marshal.SizeOf(typeof(Header)) +                    //总数据包长度Marshal.SizeOf(typeof(end_t)) +data.Length);byte[] buffer = new byte[len];                                              //buffer,用于保存帧头、数据、帧尾header.header = 0xAABB;                                                     //帧头header.cmd = (UInt16)cmd;                                                   //命令header.len = len;                                                           //总长度end.crc16 = CalculateCRC16(data, (UInt32)data.Length);                      //CRC16校验end.end = 0xCCDD;                                                           //帧尾byte[] headerBytes = StructToByteArray(header);                             //将帧头结构体转为数组byte[] endBytes = StructToByteArray(end);                                   //将帧尾结构体转为数组Array.Copy(headerBytes, 0, buffer, 0, headerBytes.Length);                  //拷贝帧头到bufferArray.Copy(data, 0, buffer, headerBytes.Length, data.Length);               //拷贝数据到bufferArray.Copy(endBytes, 0, buffer,headerBytes.Length + data.Length, endBytes.Length);              //拷贝帧尾到bufferreturn SerialPort_Send_data(buffer);                                        //串口发送数据
}

发送数据SendData的函数中,输入参数为命令CMD、数据DATA,返回值为串口发送是否成功。

具体任务:根据数据的长度申请一个完整的buffer,依次对帧头和帧尾的相关参数进行拷贝,将数据DATA进行拷贝,然后将整体buffer数据通过串口发送。

调用流程:在所有需要发送命令的操作中,生成对应的数据DATA,传入对应的CMD即可。

比如擦除ERASE命令的调用如下:

private bool Earse_SendData(UInt32 addr, UInt32 len)                          //发送擦除数据的命令
{//擦除数据的命令格式:addr(4) len(4)CmdEarse cmd = new CmdEarse                                                 //定义擦除数据命令结构体{Addr = addr,                                                            	//擦除数据命令地址Len = len                                                               	//擦除数据命令长度};byte[] buffer = StructToByteArray(cmd);                                     //将擦除命令结构体转为数组return SendData(CMD.ERASE, buffer);                                         //使用通用发送数据函数发送命令
}

4.3.2 读取数据

//通用接收数据函数,接收命令
//参数:期望接收数据cmd、数据、长度、超时时间
private STATUS GetData(CMD cmd, byte[] data, UInt32 size, int timeout)
{if (data.Length < size){labStatus.Invoke(new Action(() => { labStatus.Text = "缓冲区大小不足"; }));return STATUS.NOVAL;}if (!checkSerialPortConnected())                                            //如果串口关闭,则返回失败{labStatus.Invoke(new Action(() => { labStatus.Text = "串口未打开"; })); //跨进程设置状态栏信息return STATUS.SERIAL;}Header header = new Header();                                           //帧头end_t end = new end_t();                                                    //帧尾UInt16 len = (UInt16)(Marshal.SizeOf(typeof(Header)) +                    //总数据包长度Marshal.SizeOf(typeof(end_t)) +size);byte[] buffer = new byte[len];                                              //接收bufferheader.header = 0xAABB;                                                     //帧头header.cmd = (UInt16)cmd;                                                   //命令header.len = len;                                                           //总长度end.crc16 = 0x00;                                                           //CRC16校验end.end = 0xCCDD;                                                           //帧尾byte[] headerBytes = StructToByteArray(header);                             //期望返回的headerbyte[] endendBytes = BitConverter.GetBytes(end.end);                        //期望返回的end->end,crc16不参与sp.ReadTimeout = timeout;                                                   //设置串口的超时时间int bytesRead = 0;                                                          //当前已读的数据长度try{while (bytesRead < len)                                                 //当前已读小于总长度len,则一直读{bytesRead += sp.Read(buffer, bytesRead, len - bytesRead);           //读取剩余所需长度的数据if (bytesRead >= len)                                               //已经读满所需长度数据{byte[] read_head = new byte[headerBytes.Length];                //读到的数据帧头byte[] read_endend = new byte[endendBytes.Length];              //读到的数据帧尾Array.Copy(buffer, 0, read_head, 0, read_head.Length);          //拷贝读到的数据帧头Array.Copy(buffer, bytesRead - read_endend.Length,              //拷贝读到的数据帧尾read_endend, 0, read_endend.Length);if (!read_head.SequenceEqual(headerBytes) ||                    //将读到的数据和期望的帧头帧尾比较!read_endend.SequenceEqual(endendBytes))                    //不是期望的数据,返回错误码{labStatus.Invoke(new Action(() => { labStatus.Text = "读取串口,帧头帧尾错误"; }));return STATUS.NOVAL;}//读到的数据帧头和帧尾符合预期byte[] data_read = new byte[size];                              //读到的有效数据                 Array.Copy(buffer, read_head.Length, data_read, 0, size);       //拷贝有效数据UInt16 crc16 = CalculateCRC16(data_read, size);                 //crc16校验UInt16 crc16_read = BitConverter.ToUInt16(buffer,               //读到的crc16bytesRead - Marshal.SizeOf(typeof(end_t)));if (crc16 != crc16_read)                                        //crc16校验{labStatus.Invoke(new Action(() => { labStatus.Text = "读取串口,crc校验失败"; }));return STATUS.CRC16;}Array.Copy(data_read, data, size);                              //将有效数据拷贝到传入的datalabStatus.Invoke(new Action(() => { labStatus.Text = "读取串口成功"; }));return STATUS.OK;}}}catch (TimeoutException)                                                    //超时报错{labStatus.Invoke(new Action(() => { labStatus.Text = "读取串口超时"; }));return STATUS.TIMEOUT;}catch (Exception){labStatus.Invoke(new Action(() => { labStatus.Text = "读取串口错误"; }));}labStatus.Invoke(new Action(() => { labStatus.Text = "读取数据量不足"; })); //其他错误情况return STATUS.NOVAL;
}

读取数据GetData的函数中,输入参数为需要读取的CMD、保存数据DATA的数组、数据长度、超时时间。返回值为自定义的ERROR值,用于指定是否成功,以及对应的错误码。

此函数依次实现如下逻辑:

  1. 首先依据DATA计算出所有期望读到的数据长度len。
  2. 然后将期望读到的帧头和帧尾数据定义到数组,在读取到数据后用于判断当前数据是否合法。
  3. 循环读取数据,当数据长度够了后,依次判断帧头和帧尾是否符合预期,如果不符合预期,返回错误码。
  4. 符合预期后,再计算CRC16校验是否正确,如果不正确,则返回错误码。
  5. 最后将读取到的数据DATA拷贝到传入的参数中。

此函数中,还有一个重要逻辑超时时间timeout,可以用来设置每次读数据的超时时间,单位为ms,如果设置为0则一直等待。如果超时后,会捕获异常,直接返回错误码。

还是以EARSE命令为例,在进入此任务后,先读取UI界面将需要设置的起始地址、数据长度信息作为参数传入到Earse_SendData,随后读取数据,依据读取到的数据是否成功显示弹窗。

void EarseData()
{UInt32 addr = String2UInt32(txtStart.Text);                                 //擦除数据的起始位置UInt32 len = String2UInt32(txtLength.Text);                                 //擦除数据的长度sp.DiscardInBuffer();                                                       //清除串口缓冲区if (Earse_SendData(addr, len) != STATUS.OK)                                 //发送擦除数据的命令return;RspStatus rsp = new RspStatus();                                            //响应数据结构体byte[] buffer = new byte[Marshal.SizeOf<RspStatus>()];                      //定义一个和响应数据大小相同的bufferif (GetData(CMD.ERASE, buffer, (UInt32)buffer.Length, -1) != STATUS.OK)      //擦除大概需要21秒左右,需要一直等待,或者超时时间长一点{MessageBox.Show("数据擦除失败");                                        //获取擦除数据返回值失败,弹窗提醒return;}rsp.Addr = BitConverter.ToUInt32(buffer, 0);                                //buffer的前4个数据转换成响应数据的addrrsp.Status = BitConverter.ToUInt32(buffer, 4);                              //buffer的后4个数据转换成响应数据的状态if (rsp.Status != (UInt32)RESULT.OK)                                        //如果返回的状态不是OK,弹窗提醒{MessageBox.Show("数据擦除失败");                                        //弹窗提醒擦除失败return;}MessageBox.Show("数据擦除成功");                                            //弹窗提醒,擦除成功
}

五、下位机

5.1 硬件环境

5.1.1 硬件介绍

本例使用的主控为合宙Air32,型号为Air32F103CBT6,其最高主频可达216M。

FLASH芯片使用的是W25Q128,容量大小为16MB。

相关硬件连接图如下:

在这里插入图片描述

5.1.2 硬件资源

Air32中,使用了如下硬件资源:

  1. USART1:用于打印当前进度及调试信息,printf重定向到此串口,使用的波特率为115200。
  2. USART2:用于和上位机软件进行通信,保存和读取FLASH的数据等任务,波特率为921600。
  3. TIMER4:用于和USART2配合,实现以时间间隔判断帧的功能。
  4. SPI1:用于和FLASH芯片通信,引脚为PA5、PA6、PA7,CS引脚为PA4。

5.1.3 内存资源

本例中使用了4个字库,加上字库头总大小为3,238,697字节,大约为3.09M。

FLASH芯片总容量为16M,将数据保存在后4M空间中,即起始地址为0x00C00000,长度为0x00316B29。

5.2 以时间间隔判断帧

以时间间隔判断帧是一种简单且方便的判断帧方法,和类似说话一样,在一段时间(时间间隔)内没有说话后,将之前听到的话(接收到的数据)进行处理,以时间间隔1ms为例,以时间间隔判断帧的主要原理如下:

  1. 将定时器的中断响应时间设置为1ms,并关闭定时器。
  2. 在串口收到第一字节数据时,启动定时器,并将数据保存到指定的缓冲buffer中。
  3. 每次收到新数据时,将定时器计数值清零。
  4. 停止发送数据后,定时器就会进入定时器中断,此时buffer中收到的数据即为一帧完整的数据。

此方法在串口协议中较为常用,且实现简单。

5.3 软件流程

在这里插入图片描述

在主循环中一直监听,如果USART2收到一帧完整数据,则调用fontupd_process进行处理,随后调用fontupd_process对数据包进行解析,判断HAED、END是否符合预期,做CRC16校验,获取CMD、LEN、DATA等关键信息,随后调用具体的命令函数进行处理,并根据协议发送响应数据包。

5.4 重要函数

5.4.1 字体初始化

//初始化字体
//返回值:0,字库完好.
//		 其他,字库丢失
u8 fontudp_init_and_check(void)
{u8 t=0;W25QXX_Init();while(t++ < 10)//连续读取10次,都是错误,说明确实是有问题,得更新字库了{W25QXX_Read((u8*)&ftinfo, fontaddr, sizeof(ftinfo));//读出ftinfo结构体数据if(ftinfo.fontok == 0XAA)break;delay_ms(20);}if (ftinfo.fontok != 0XAA)return 1;return 0;
}

对FLASH进行初始化,并读取字库头,判断当前字库是否存在。

5.4.2 串口数据处理

//数据包处理,传入串口收到的数据,返回处理结果。
int fontupd_process(u8 *buffer, int size)
{struct fontupd_config_t config = {0};int ret = -1;ret = fontupd_parse(buffer, size, &config);if(ret != 0)return ret;switch (config.cmd) {case CMD_GETINFO:return flash_get_info(config.data, config.len_data);case CMD_ERASE_CHIP:return flash_erase_chip(config.data, config.len_data);case CMD_ERASE:return flash_erase(config.data, config.len_data);case CMD_SAVE:return flash_save(config.data, config.len_data);case CMD_READ:return flash_read(config.data, config.len_data);default:printf("not support cmd(%d)\r\n", config.cmd);break;}return 0;
}

在此函数中,传入的是串口收到的数据以及数据长度,进入函数后,首先对数据包进行解析,并针对命令选择对应的flash操作功能。

5.4.3 协议解析

static int fontupd_parse(u8 *buffer, int size, struct fontupd_config_t *config)
{//buffer为空,错误if (buffer == NULL)return -1;//数据长小于头+尾,数据量不足,返回if(size < sizeof(struct header_t) + sizeof(struct end_t)){printf("%s: error size(%d) need(%d)byte at least\r\n", __func__, size, sizeof(struct header_t) + sizeof(struct end_t));return -1;}//获取头struct header_t *header = (struct header_t *)buffer;if(header->header.value != 0xAABB){printf("%s: error header(0x%04X)\r\n", __func__, header->header.value);return -1;}//获取总长度int len = header->len.value;if(len != size){printf("%s: error len(%d - 0x%04X) size(%d - 0x%04X)\r\n", __func__, len, len, size, size);return -1;}//获取数据及长度int len_data = len - sizeof(struct header_t) - sizeof(struct end_t);u8 *data = len_data ? buffer + sizeof(struct header_t) : NULL;//获取尾struct end_t *end = (struct end_t *)(buffer + sizeof(struct header_t) + len_data);if(end->end.value != 0xCCDD){printf("%s: error end(0x%04X)\r\n", __func__, end->end.value);return -1;}uint16_t crc16 = calculateCRC16(data, len_data);printf_debug("%s: len(%d - 0x%04X) len_data(%d - 0x%04X) crc(%04X %04X)\r\n", __func__, len, len, len_data, len_data, end->crc16.value, crc16);if(end->crc16.value != crc16){printf("%s: error crc(%04X %04X)\r\n", __func__, end->crc16.value, crc16);printf_buffer_debug(buffer, size);return -1;}config->cmd = header->cmd.value;config->data = data;config->len_data = len_data;return 0;
}

在此函数中,根据第二章的通信协议进行解析,并将解析出的CMD、数据、数据长度信息返回。

5.4.4 获取信息

static int flash_get_info(u8 *buffer, int size)
{printf("%s: enter size(%d)\r\n", __func__, size);//数据长为0if(size != 0){printf("%s: error size(%d) need(%d)\r\n", __func__, size, 0);return -1;}struct get_info_t info;info.id.value = W25QXX_TYPE;info.size.value = FLASH_SIZE;send_rsp(CMD_GETINFO, (u8 *)&info, sizeof(struct get_info_t));printf("%s: exit\r\n", __func__);return 0;
}

5.4.5 整片擦除

static int flash_erase_chip(u8 *buffer, int size)
{printf("%s: enter size(%d)\r\n", __func__, size);struct res_t res;u16 status = STATUS_ERR;//数据长为0if(size != 0){printf("%s: error size(%d) need(%d)\r\n", __func__, size, 0);goto exit;}W25QXX_Erase_Chip();status = STATUS_OK;exit:res.addr.value = fontaddr;res.status.value = status;send_rsp(CMD_ERASE_CHIP, (u8 *)&res, sizeof(struct res_t));printf("%s: exit\r\n", __func__);return 0;
}

5.4.6 指定擦除

static int flash_erase(u8 *buffer, int size)
{printf("%s: enter size(%d)\r\n", __func__, size);struct erase_data_header_t *header = NULL;struct res_t res;u16 status = STATUS_ERR;//buffer为空,错误if (buffer == NULL){goto exit;}//数据长小于头,数据量不足,返回if(size < sizeof(struct erase_data_header_t)){printf("%s: error size(%d) need(%d)\r\n", __func__, size, sizeof(struct erase_data_header_t));goto exit;}//获取头header = (struct erase_data_header_t *)buffer;printf("%s: addr(0x%08X) len(0x%08X)\r\n", __func__, header->addr.value, header->len.value);W25QXX_Erase(header->addr.value, header->len.value);status = STATUS_OK;exit:res.addr.value = header->addr.value;res.status.value = status;send_rsp(CMD_ERASE, (u8 *)&res, sizeof(struct res_t));printf("%s: exit\r\n", __func__);return 0;
}

5.4.7 保存数据

static int flash_save(u8 *buffer, int size)
{struct res_t res;//buffer为空,错误if (buffer == NULL)return -1;//数据长小于头,数据量不足,返回if(size < sizeof(struct save_data_header_t)) {printf("%s: error size(%d) need(%d)\r\n", __func__, size, sizeof(struct save_data_header_t));return -1;}//获取头struct save_data_header_t *header = (struct save_data_header_t *)buffer;//获取地址和长度u32 addr = header->addr.value;u16 len = header->len.value;if (len + sizeof(struct save_data_header_t) != size) {printf("%s: error len(%d - 0x%04X) size(%d - 0x%04X)\r\n", __func__, len, len, size, size);return -1;}u8 *data = buffer + sizeof(struct save_data_header_t);printf_debug("%s: addr(0x%08X) len(%d - 0x%04X)\r\n", __func__, addr, len, len);W25QXX_Write(data, addr, len);		//从0开始写入4096个数据res.addr.value = header->addr.value;res.status.value = STATUS_OK;send_rsp(CMD_SAVE, (u8 *)&res, sizeof(struct res_t));return 0;
}

5.4.8 读取数据

static int flash_read(u8 *buffer, int size)
{u8 data[4096];//buffer为空,错误if (buffer == NULL)return -1;//数据长小于头,数据量不足,返回if(size < sizeof(struct read_data_header_t)) {printf("%s: error size(%d) need(%d)\r\n", __func__, size, sizeof(struct erase_data_header_t));return -1;}//获取头struct read_data_header_t *header = (struct read_data_header_t *)buffer;//获取地址和长度u32 addr = header->addr.value;u32 len = header->len.value;printf_debug("%s: addr(0x%08X) len(0x%08X)\r\n", __func__, addr, len);if (len > 4096) {printf("%s: not support read len(%d) bytes\r\n", __func__, len);return -1;}W25QXX_Read(data, addr, len);send_rsp(CMD_READ, data, len);return 0;
}

5.4.9 发送响应数据

static void send_rsp(int cmd, u8 *data, u16 len)
{struct header_t header;struct end_t end;header.header.value = 0xAABB;header.cmd.value = cmd;header.len.value = sizeof(struct header_t) + sizeof(struct end_t) + len;end.crc16.value = calculateCRC16(data, len);;end.end.value = 0xCCDD;USART2_Send_N((char*)&header, sizeof(struct header_t));USART2_Send_N((char*)data, len);USART2_Send_N((char*)&end, sizeof(struct end_t));
}

六、其他

6.1 参考文献

本项目参考并使用了正点原子关于字库的部分。

6.2 开源

该项目首发于https://blog.csdn.net/chouye5700?type=blog、https://gitee.com/wuzjjj/fontupd

6.2.1 开源协议

The MIT License (MIT)Copyright (c) [2025] [wuzjjj]Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

6.2.2 资料

链接: https://pan.baidu.com/s/1eE0OCbkzDMeLyo-L9I4FTg?pwd=6666 提取码: 6666

6.3 扩展

本例可非常方便的移植到STM32上,只需要将FONTUPD、USART、TIMER中重要函数及变量进行移植即可。

也可以将本项目用于更新其他FLASH信息。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/91435.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/91435.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/91435.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Lombok常用注解及功能详解

Lombok常用注解及功能详解一、Lombok简介与环境配置1.1 什么是Lombok&#xff1f;1.2 环境配置1.2.1 Maven项目1.2.2 Gradle项目1.2.3 IDE配置&#xff08;关键&#xff09;二、Lombok常用注解详解2.1 Data&#xff1a;一站式生成核心方法2.2 Getter/Setter&#xff1a;单独生成…

应用分层

应用分层是⼀种软件开发设计思想&#xff0c;它将应用程序分成N个层次&#xff0c;这N个层次分别负责各自的职责&#xff0c; 多个层次之间协同提供完整的功能。根据项目的复杂度&#xff0c;把项目分成三层&#xff0c;四层或者更多层。常见的MVC设计模式&#xff0c;就是应用…

[特殊字符] 【JAVA进阶】StringBuilder全方位解析:从使用到源码,一文搞定!

&#x1f525; 掌握StringBuilder&#xff0c;让你的Java字符串操作性能飙升&#xff01;&#x1f9e9; StringBuilder是什么&#xff1f; StringBuilder是Java中用于动态构建字符串的可变字符序列类&#xff0c;位于java.lang包中。与不可变的String类不同&#xff0c;StringB…

Redis 数据结构全景解析

Redis 不是简单的 key-value 缓存&#xff0c;它更像一把“瑞士军刀”。 只要掌握数据结构&#xff0c;就能把同一份内存用出 10 倍效率。0. 开场白&#xff1a;为什么聊数据结构&#xff1f; 面试常问“Redis 有几种数据类型&#xff1f;”——很多人答 5 种&#xff08;Strin…

ansible.cfg 配置文件的常见配置项及其说明

配置项说明默认值defaults默认配置部分inventory指定清单文件的位置&#xff0c;可以是文件路径、目录或动态清单脚本。/etc/ansible/hostsremote_user默认的远程用户roothost_key_checking是否启用主机密钥检查。设置为 False 跳过 SSH 主机密钥验证。Trueask_pass是否在执行时…

Effective C++ 条款15:在资源管理类中提供对原始资源的访问

Effective C 条款15&#xff1a;在资源管理类中提供对原始资源的访问核心思想&#xff1a;RAII类需要提供访问其封装原始资源的显式或隐式接口&#xff0c;以兼容需要直接操作资源的API&#xff0c;同时维持资源的安全管理。 ⚠️ 1. 原始资源访问的必要性 使用场景示例&#x…

Linux 进程管理与计划任务设置

Linux 进程管理与计划任务设置一、进程管理进程管理用于监控、控制系统中运行的程序&#xff08;进程&#xff09;&#xff0c;包括查看进程状态、调整优先级、终止异常进程等。以下是核心命令及操作说明&#xff1a;1. 常用进程查看命令&#xff08;1&#xff09;ps&#xff1…

MYSQL数据库之索引

1、引入索引的问题在图书馆查找一本书的过程&#xff0c;可类比数据库查询场景。在一般软件系统中&#xff0c;对数据库操作以查询为主&#xff0c;数据量较大时&#xff0c;优化查询是关键&#xff0c;索引便是优化查询的重要手段 。2、索引是什么索引是一种特殊文件&#xff…

ArcGIS以及ArcGIS Pro如何去除在线地图制作者名单

问题&#xff1a;ArcGIS和ArcGIS Pro提供了许多在线地图服务&#xff0c;但是这些地图会自动生成制作者名单&#xff0c;如下图所示&#xff1a; 在线地图加载方式可参考&#xff1a;如何在ArcGIS和ArcGIS Pro中添加在线底图 这在出图时有时会造成图的部分信息遮挡或出图不美观…

InfluxDB 与 Golang 框架集成:Gin 实战指南(二)

四、实际应用案例4.1 案例背景某智能工厂部署了大量的物联网设备&#xff0c;如传感器、智能仪表等&#xff0c;用于实时监测生产线上设备的运行状态、环境参数&#xff08;如温度、湿度&#xff09;以及生产过程中的各项指标&#xff08;如产量、次品率&#xff09;。这些设备…

Linux系统磁盘未分配的空间释放并分配给 / 根目录的详细操作【openEuler系统】

选择 Fix 修正 GPT 表 输入 Fix 并按回车&#xff0c;parted 会自动&#xff1a; 扩展 GPT 表的 结束位置 到磁盘末尾。释放未被使用的空间&#xff08;1048576000 个 512B 块&#xff0c;约 500GB&#xff09;。 验证修正结果 修正后&#xff0c;再次运行&#xff1a; parted …

王道考研-数据结构-01

数据结构-01视频链接&#xff1a;https://www.bilibili.com/video/BV1b7411N798?spm_id_from333.788.videopod.sections&vd_source940d88d085dc79e5d2d1c6c13ec7caf7&p2 数据结构到底在学什么? 数据结构这门课他要学习的就是怎么用程序代码把现实世界的问题给信息化&…

k8s云原生rook-ceph pvc快照与恢复(上)

#作者&#xff1a;Unstopabler 文章目录前言部署rook-ceph on kubernets条件Ceph快照概述什么是PVC安装快照控制器和CRD1.安装crds资源2.安装控制器3.安装快照类前言 Rook 是一个开源的云原生存储编排器&#xff0c;为各种存储解决方案提供平台、框架和支持&#xff0c;以便与…

springcloud04——网关gateway、熔断器 sentinel

目录 注册中心 nacos | eurekaServer |zookeeper(dubbo) 配置中心 nacos | config Server 远程服务调用 httpClient | RestTemplate | OpenFeign 负载均衡服务 ribbon | loadbalancer 网关 zuul | gateway 熔断器 hystrix | sentinel 网关 sentinel 流控 压测工具 1…

XSS跨站脚本攻击详解

一、XSS攻击简介跨站脚本攻击的英文全称是Cross-Site Scripting&#xff0c;为了与CSS有所区别&#xff0c;因此缩写为“XSS”由于同源策略的存在&#xff0c;攻击者或者恶意网站的JavaScript代码没有办法直接获取用户在其它网站的信息&#xff0c;但是如果攻击者有办法把恶意的…

Linux /proc/目录详解

文章目录前言文件说明注意事项前言 在 Linux 系统中&#xff0c;/proc 目录是一个特殊的虚拟文件系统&#xff0c;它提供了对系统内核和进程的访问。/proc 目录中的文件和目录不是真实存在的&#xff0c;它们是在运行时由内核动态生成的&#xff0c;用于提供系统和进程的相关信…

北斗变形监测在地质灾害监测中的应用

内容概要 北斗形变监测系统在地质灾害监测领域发挥着核心作用&#xff0c;该系统基于北斗卫星导航技术&#xff0c;实现对地表变形的精确追踪。通过毫米级精度定位能力&#xff0c;北斗形变监测技术为滑坡等灾害提供关键数据支撑&#xff0c;尤其在偏远地区应用中&#xff0c;单…

2025新征程杯全国54校园足球锦标赛在北京世园公园隆重开幕

2025年8月1日&#xff0c;备受瞩目的2025新征程杯全国54校园足球锦标赛&#xff08;北京&#xff09;在北京世园公园盛大拉开帷幕。开幕式上&#xff0c;中国关心下一代健康体育基金会副秘书长、中国青少年研究会理事、全国 54 校园足球人才培养计划创始人何占强主任表示&#…

分类预测 | Matlab实现CPO-PNN冠豪猪算法优化概率神经网络多特征分类预测

分类预测 | Matlab实现CPO-PNN冠豪猪算法优化概率神经网络多特征分类预测 目录分类预测 | Matlab实现CPO-PNN冠豪猪算法优化概率神经网络多特征分类预测分类效果基本介绍程序设计分类效果 基本介绍 1.Matlab实现CPO-PNN冠豪猪算法优化概率神经网络多特征分类预测&#xff0c;运…

机器学习——逻辑回归(LogisticRegression)的核心参数:以约会数据集为例

理解 LogisticRegression 的核心参数&#xff1a;以约会数据集为例 逻辑回归&#xff08;Logistic Regression&#xff09;是机器学习中一种基础且重要的分类算法&#xff0c;特别适用于解决二分类和多分类问题。本文将基于 sklearn.linear_model.LogisticRegression 的用法&a…