目录
效果展示:
【移植】GD32 FATFS SDIO 播放二进制Bad Apple
准备工作:
GD32F4xx官方固件库
调试好可使用的OLED屏幕(本文基于0.96'单色OLED)
<=16GB的TF卡 并格式化
Image2LCD 取模软件
一个Linux系统用于合并中间文件
一、SDIO
这一部分主要是将官方固件库中给出的SDIO例程整合到自已的工程里,要点如下:
- 首先找到SDIO的示例文件夹位置GD32F4xx_Firmware_Library_V3.0.2\GD32F4xx_Firmware_Library\Examples\SDIO\Read_write ,在代码编辑器中打开这个文件夹。在这里我们需要复制的文件是以下sdcard.c 和sdcard.h文件,我们将其复制到自己的工程下,这里我放在Hardware下的SDIO文件中。在Keil中将文件添加进工程中。
- 打开SDIO\Read_write\main.c文件,首先是定义了一些变量和函数声明,我们可以直接将其复制进工程中。接着,将三个函数体也复制到工程中。
sd_card_info_struct sd_cardinfo; /* information of SD card */ uint32_t buf_write[512]; /* store the data written to the card */ uint32_t buf_read[512]; /* store the data read from the card */ void nvic_config(void); sd_error_enum sd_io_init(void); void card_info_get(void);
- 回到我们的工程中,在main.c文件的main函数中添加以下代码
sd_error_enum res; //用于接收SD卡相关信息 res = sd_io_init(); if(SD_OK != res) { printf("初始化失败,请检查代码或SD卡..."); while(1); } else { card_info_get(); }
这段代码创建一个存储SD卡相关操作错误信息的变量,然后对SD卡进行初始化,如果初始化成功,会在串口打印SD卡的相关信息,也就说明SD卡初始化成功了。编译后将程序下载到单片机,打开串口工具,按下复位键可以看到SD卡初始化的相关信息。偶现初始化失败,但再复位就OK,常见于单片机初次上电。初始化成功串口打印信息如下 :
Card information:
## Card version 4.xx ##
## SDHC card ##
## Device size is 15558144KB ##
## Block size is 512B ##
## Block count is 31116288 ##
## CardCommandClasses is: 5b5 ##
## Block operation supported ##
## Erase supported ##
## Lock unlock supported ##
## Application specific supported ##
## Switch function supported ##
注意:移植SDIO的部分与移植FATFS的部分相互独立,也就是说在移植FATFS中会对SDIO部分做相应改动。
二、移植FATFS
现在移植FATFS文件管理系统,GD32的固件库中也提供了相关的文件,在GD32F4xx_Firmware_Library_V3.0.2\GD32F4xx_Firmware_Library\Utilities\Third_Party\fat_fs下有两个文件夹inc和src,将这两个文件夹复制到自己的工程中,其他的三个txt文档不用复制。
回到我们的工程中添加src和inc中的文件,编译运行。
注意:编译有很多error,请先确认你用的是激活过的keil,因为FATFS系统有点大会超过社区版上限,还会报类似于L6050U这类错误代码。现在来看错误代码:
7个错误中有6个要求调整至C99模式,1个error是找不到usb_conf.h。
我们先解决第一个,勾选C99选项。
第二个我们直接在报错文件integer.h中注释掉该语句即可。
编译无错误后就可以进行下一步了,FATFS已经将底层代码写好了,只需要修改提供的几个接口函数即可。
在这里只需要修改diskio.c即可,但我们首先打开diskio.h文件看有什么函数需要修改。
DSTATUS disk_initialize (BYTE pdrv);
DSTATUS disk_status (BYTE pdrv);
DRESULT disk_read (BYTE pdrv, BYTE* buff, DWORD sector, UINT count);
DRESULT disk_write (BYTE pdrv, const BYTE* buff, DWORD sector, UINT count);
DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void* buff);
DWORD get_fattime (void);
显而易见,他们分别是用于实现初始化、获取状态、读写和控制功能的函数,我们逐个进行修改。
在此之前,我们先进行一下宏定义,其中ATA代表我们的SD卡,盘符为0,块的大小为512,busmode为4bit。
#define ATA 0
#define SPI_FLASH 1
#define BLOCKSIZE 512
#define BUSMODE_4BIT
宏定义后,我们看disk_status函数,回到diskio.c。
//修改之前
DSTATUS disk_status (
BYTE pdrv /* Physical drive number to identify the drive */
)
{
DSTATUS stat;
stat = disk.drv[pdrv]->disk_status(disk.lun[pdrv]);
return stat;
}
传入一个prdv参数用于区分磁盘,接着读取磁盘的状态并返回。因为我们在宏定义中定义了两块磁盘,我们选择switch函数来选择,在返回状态前,一定要使用FATFS里的DSTATUS类型,因为GD的库返回的参数会有些许的不同,包括后边的一些函数也是,这是最容易出错的一点。修改后的代码如下
//修改之后
DSTATUS disk_status (
BYTE pdrv /* Physical drive number to identify the drive */
)
{
DSTATUS stat = STA_NOINIT;
switch(pdrv)
{
case ATA:
stat = RES_OK;
break;
case SPI_FLASH:
stat = STA_NOINIT;
}
return stat;
}
再来看 disk_initialize函数,原始代码非常简单,我们要做的是在这一步将我们的SD卡给初始化。
//修改之前
DSTATUS disk_initialize (
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
DSTATUS stat = RES_OK;
if (disk.is_initialized[pdrv] == 0) {
disk.is_initialized[pdrv] = 1;
stat = disk.drv[pdrv]->disk_initialize(disk.lun[pdrv]);
}
return stat;
}
既然这一步主要是SD卡初始化,我们就去看样例SDIO是怎样初始化的,因为我们在第二步中已经将函数复制到main.c中了,我们直接去找相关函数,可以发现初始化函数为sd_io_init()函数。分析一下这个函数的流程
SD卡初始化 -> 获取SD卡信息 -> 选择指定SD卡 -> SD卡状态获取 -> SD卡总线模式选择 -> SD卡传输模式选择
我们可以在switch选中我们的SD卡后 复制绝大多数代码过去,但我们要按照FATFS的格式返回标志信息,所以我们需要手动return一下,修改后的代码如下(为了debug方便我加了一写printf,不需要可以删除):
extern sd_card_info_struct sd_cardinfo;//使用main.c中存储SD卡信息的变量
DSTATUS disk_initialize (
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
DSTATUS stat = STA_NOINIT;
uint32_t cardstate = 0;
switch(pdrv)
{
case ATA:
printf("SD_Init...\r\n");
stat = sd_init();
if(SD_OK == stat)
{
stat = sd_card_information_get(&sd_cardinfo);
printf("sd_card_information_get\r\n");
}else
{
return STA_NOINIT;
}
if(SD_OK == stat)
{
stat = sd_card_select_deselect(sd_cardinfo.card_rca);
printf("sd_card_select_deselect\r\n");
}else
{
return STA_NOINIT;
}
stat = sd_cardstatus_get(&cardstate);
if(cardstate & 0x02000000)
{
while(1)
{}
}
if(SD_OK == stat)
{
#ifdef BUSMODE_4BIT
stat = sd_bus_mode_config(SDIO_BUSMODE_4BIT);
#else
stat = sd_bus_mode_config(SDIO_BUSMODE_1BIT);
#endif
printf("sd_bus_mode_config\r\n");
}else
{
return STA_NOINIT;
}
if(SD_OK == stat)
{
#ifdef DMA_MODE
stat = sd_bus_mode_config(SD_DMA_MODE);
nvic_irq_enable(SDIO_IRQn,0,0);
#else
stat = sd_transfer_mode_config(SD_POLLING_MODE);
printf("SD_POLLING_MODE\r\n");
#endif
}else
{
return STA_NOINIT;
}
if(SD_OK == stat)
{
return 0;
printf("FATFS Init Success...\r\n");
}else
{
return STA_NOINIT;
}
default:
stat = STA_NOINIT;
}
return stat;
}
回到main.c文件中,将sd_io_init()整个函数体删掉,因为我们在FATFS中进行了初始化,同时main函数里也要进行修改。
接着来看disk_read函数
DRESULT disk_read (
BYTE pdrv, /* Physical drive nmuber to identify the drive */
BYTE *buff, /* Data buffer to store read data */
DWORD sector, /* Sector address in LBA */
UINT count /* Number of sectors to read */
)
{
DRESULT res;
res = disk.drv[pdrv]->disk_read(disk.lun[pdrv], buff, sector, count);
return res;
}
传入的参数有磁盘编号,保存的buff地址,磁盘中对应扇区的位置,要读取的扇区数。我们首先在这里对传入的参数进行一次验证,包括buff地址和要读取的扇区数,防止读取出现问题。选择好磁盘后,对于要读取的扇区数分两种情况,单个扇区和多个扇区两个读取函数。这样我们就可以进行代码的编写了。注意,在GD的读取扇区的库函数中针对传入数据格式有要求,因此需要进行强制类型转换。
DRESULT disk_read (
BYTE pdrv, /* Physical drive nmuber to identify the drive */
BYTE *buff, /* Data buffer to store read data */
DWORD sector, /* Sector address in LBA */
UINT count /* Number of sectors to read */
)
{
sd_error_enum status = SD_ERROR;
if(NULL == buff)
{
return RES_PARERR;
}
if(!count)
{
return RES_PARERR;
}
switch(pdrv)
{
case ATA:
if(count == 1)
{
status = sd_block_read((uint32_t*)(&buff[0]), (uint32_t)(sector<<9),BLOCKSIZE);
}
else
{
status = sd_multiblocks_read((uint32_t*)(&buff[0]), (uint32_t)(sector<<9),BLOCKSIZE,(uint32_t)count);
}
if(SD_OK == status)
{
return RES_OK;
}
return RES_ERROR;
default:
return RES_PARERR;
}
}
写入函数disk_write也是如此,这里直接给出代码
DRESULT disk_write (
BYTE pdrv, /* Physical drive nmuber to identify the drive */
const BYTE *buff, /* Data to be written */
DWORD sector, /* Sector address in LBA */
UINT count /* Number of sectors to write */
)
{
sd_error_enum status = SD_ERROR;
if(!count){
return RES_PARERR;
}
switch(pdrv)
{
case ATA:
if(count == 1)
{
status = sd_block_write((uint32_t*)buff, sector<<9,BLOCKSIZE);
}
else
{
status = sd_multiblocks_read((uint32_t*)buff,sector<<9,BLOCKSIZE,(uint32_t)count);
}
if(SD_OK == status)
{
return RES_OK;
}
return RES_ERROR;
default:
return RES_PARERR;
}
}
接着是diskio.c最后一个函数disk_ioctl
DRESULT disk_ioctl (
BYTE pdrv, /* Physical drive nmuber (0..) */
BYTE cmd, /* Control code */
void *buff /* Buffer to send/receive control data */
)
{
DRESULT res;
res = disk.drv[pdrv]->disk_ioctl(disk.lun[pdrv], cmd, buff);
return res;
}
这个函数最重要的就是cmd参数了,我们可以在diskio.h中找到相关定义
/* Generic command (Used by FatFs) */
#define CTRL_SYNC 0 /* Complete pending write process (needed at _FS_READONLY == 0) */
#define GET_SECTOR_COUNT 1 /* Get media size (needed at _USE_MKFS == 1) */
#define GET_SECTOR_SIZE 2 /* Get sector size (needed at _MAX_SS != _MIN_SS) */
#define GET_BLOCK_SIZE 3 /* Get erase block size (needed at _USE_MKFS == 1) */
#define CTRL_TRIM 4 /* Inform device that the data on the block of sectors is no longer used (needed at _USE_TRIM == 1) */
CTRL_SYNC在本项目中并不涉及到,因此直接退出即可;GET_SECTOR_COUNT可以通过库函数经过相关变换求得;GET_SECTOR_SIZE即SD卡的块大小,在前边初始化已经获取过,直接引用即可;GET_BLOCK_SIZE是我们宏定义的块大小,在这里是512。实现的代码如下
DRESULT disk_ioctl (
BYTE pdrv, /* Physical drive nmuber (0..) */
BYTE cmd, /* Control code */
void *buff /* Buffer to send/receive control data */
)
{
uint32_t capacity;
DRESULT status = RES_PARERR;
switch (pdrv)
{
case ATA:
switch (cmd)
{
case GET_SECTOR_SIZE:
*(WORD * )buff = BLOCKSIZE;
break;
case GET_BLOCK_SIZE:
*(DWORD * )buff = sd_cardinfo.card_blocksize;
break;
case GET_SECTOR_COUNT:
capacity = sd_card_capacity_get();
*(DWORD * )buff = capacity*1024/sd_cardinfo.card_blocksize;
break;
case CTRL_SYNC:
break;
}
return RES_OK;
break;
default:
status = RES_PARERR;
break;
}
return status;
}
因为在FATFS中使用的栈大小超出了预先的设计,因此我们需要在启动文件startup_gd32f450_470.s中队栈的默认大小进行修改,修改后代码如下
; <h> Stack Configuration
; <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>
Stack_Size EQU 0x00001000
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
至此,FATFS移植已经完成,下面将进行相关的FATFS文件系统使用。
回到main.c函数中,编写SD卡和FATFS初始化函数sd_fatfs_init,因为我们已经在diskic中编写过初始化代码了,因此在初始化时只需要进行挂在磁盘即可,记得配合相关的异常处理函数。
void sd_fatfs_init (void)
{
uint16_t result = 0;
result = f_mount(&fs,"0:",1);
if(result == FR_NO_FILESYSTEM)
{
printf("No file system in SD...\r\n");
while (1);
}
else if(result != FR_OK)
{
printf("File mount error for SD! Result:(%d)\r\n",result);
printf("Initilization error maybe!\r\n");
while (1);
}
else
{
printf("File system existed in SD!\r\n");
}
}
在主函数里调用初始化函数,FATFS文件管理系统就已经完成初始化并可以使用了.
三、视频取模与代码实现
关于视频取模与代码的实现可以参考这位大佬
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:0.96寸oled显示坏苹果(badapple)_点灯大师~的博客-CSDN博客
在这位大佬第三步批量生成bin的步骤中,我需要补充一点内容.
推荐使用Linux进行文件合并,Windows下会出现很奇怪的问题,比如莫名其妙文件大小对不起来,最后播放的时候导致错位,频闪等。
将生成的batch文件夹压缩成压缩包后传到Linux中,使用
cat $(ls -v pic*.bin) > badapple.bin
命令合并成badapple.bin 文件 。这一步命令是将生成的文件按照升序合并。合并完后就可以拷贝到SD卡中了。
播放二进制文件可以参考大佬的函数,注意一下延时函数,显示函数等是否和自己的匹配,进行一定的修改。也可以下载我的代码编译运行。
下面给一下我的踩坑经历:
- FATFS下SD卡始终挂载失败,检查启动文件栈大小是否修改。
- 播放时跳帧,检查合并bin文件的时候是否按顺序合并,Windows和Linux均会出现问题,因此要使用上边的代码按顺序合成。
- 播放时错位,重新批量生成bin文件并合并,重新来一次。
- 上电后播放,按下复位键后检查串口输出数据,根据串口输出的数据进行debug。
- 部分开发板偶现无法下载程序的问题,重新下载,移走手机等电磁干扰严重的电器。