打印
[STM32F4]

基于STM32(串口+DMA+空闲中断+环形缓冲区)实现 YMODEM协议IAP在线烧写程序

[复制链接]
5641|5
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
本帖最后由 zero949079783 于 2024-7-14 10:27 编辑

基于STM32(串口+DMA+空闲中断+环形缓冲区)实现 YMODEM协议IAP在线烧写程序
BootLoader 扇区: 0x08000000  - 0x08004000 共16K大小

生成BIN: fromelf.exe --bin -o "$L@L.bin" "#L

上位机软件:SecureCRT



Ymodem_STM32F1:
链接:https://pan.baidu.com/s/1SO34uS3flsdE8MdL4HHU0g?pwd=802v
提取码:802v
Ymodem_STM32F4:
链接:https://pan.baidu.com/s/13Kh6QzgNosYnYE5fJ-TB4Q?pwd=u5tj
提取码:u5tj
https://gitee.com/csx949089783/ymodem_-stm32-f4.git









YMODED接口函数:
#include "ymodem.h"
#include "crc.h"
#include "usart.h"
#include "inner_flash.h"
#include "delay.h"
#include "key.h"
#define YMODEM_TIME 10


typedef  void (*pFunction)(void);
pFunction Jump_To_Application;


typedef struct{
        uint8_t OTA_FLAG :          1;
        uint8_t YMODE_FLAG : 1;
        uint8_t YMODE_Frist_FLAG :1;
        uint8_t YMODE_Frist_Pack_FLAG: 1;
        uint8_t YMODE_EOH_STX_DATALEN_FLAG :1;               
        uint8_t fileName[256];                                                                                //文件名        
        uint8_t  UpdataBuffer[STM32_PAGE_SIZE];                //接收数据缓冲区        
        uint16_t time;                                                                                                                                        //OTA退出时间
        uint16_t count;                                                        
        uint32_t num;                                                                                                                        //接收数据包数
        uint32_t STXnum;                                                                                                        //接收数据包数               
        uint32_t crc;                                                                                                                        //CRC校验
        uint32_t remanider;                                                                                                //数据余数
        uint32_t packnum;                                                                                                        //数据总包数
        uint32_t remaniderpacknum;                                                                //数据余数总包数(1024时)
        uint32_t fileSize;                                                                                                //文件大小
        uint32_t JumpAddress;                                                                                        //APP跳转地址
}YMODEM_T ;

YMODEM_T ymodem_t;

void Ymodem_Init(void)
{
        memset(&ymodem_t,0,sizeof(ymodem_t));
        ymodem_t.time = YMODEM_TIME;
}


static void BootLoader_To_App(void)
{
        Delay_ms(1);
        printf("\r\n");
        printf("BootLoader_To_App.....\r\n");
        Delay_ms(1);
        FLASH_Lock();
        
        SysTick->CTRL &= SysTick_CTRL_TICKINT_Msk;        //关滴答定时器
        
  ymodem_t.JumpAddress = *(__IO uint32_t*) (APPLICATION_ADDRESS + 4);        
  /* Jump to user application */
  Jump_To_Application = (pFunction) ymodem_t.JumpAddress;
  /* Initialize user application's Stack Pointer */
  __set_MSP(*(__IO uint32_t*) APPLICATION_ADDRESS);
  Jump_To_Application();
}

static void BootLoader_SendByte(uint8_t data)
{
        Usartx_SendArray(USART1,&data,1);
}

/*************************************************************************
*        函 数 名: BootLoader_EraseFlash
*        功能说明: Flash扇区擦除
*        形    参:startSectorAddr:起始扇区地址    numSector:扇区个数
*        返 回 值: 无
**************************************************************************/
void BootLoader_EraseFlash(void)
{
        STM32_EraseFlash(STM32_A_SADDR,11);
}

/*************************************************************************
*        函 数 名: BootLoader_WriteFlash
*        功能说明: 数据写入
*        形    参:startAddr:起始地址   writeData:写入数据   numByte:写入字节数
*        返 回 值: 无
**************************************************************************/
static void BootLoader_WriteFlash(uint32_t startAddr, uint32_t* writeData, uint32_t numByte)
{
        STM32_WriteFlash(startAddr,writeData,numByte);
}



void BootLoader_Meun(void)
{
        static uint16_t times = 0;
        times ++;

         if(ymodem_t.OTA_FLAG == YMODEM_DISABLE && times%1000 == 0)
                {               
                        ymodem_t.time --;
                        
                        printf(" \r\n %ds后退出BootLoader\r\n ",ymodem_t.time);
                        if(ymodem_t.time <= 0)
                        {
                                BootLoader_To_App();
                        }
                }
                else if(ymodem_t.YMODE_FLAG == YMODEMD_ENABLE && times %500 == 0)
                {
                        ymodem_t.time = YMODEM_TIME;
                        BootLoader_SendByte(CA);        //发送握手
                }

        //按键双击进入OTA
        if( get_Key_Val() == 0x51 && ymodem_t.YMODE_FLAG == YMODEM_DISABLE && ymodem_t.OTA_FLAG == YMODEM_DISABLE)
        {        
                        printf("EraseFlash......\r\n");
                        BootLoader_EraseFlash();
                        printf("EraseFlash successfully \r\n");
                        ymodem_t.OTA_FLAG = 1;
                        ymodem_t.YMODE_FLAG = 1;
        }


}

static void BootLoader_Com(uint8_t *data)
{
        if(memcmp(data,"1",1)==0 )        //当接收到上位机或者双击时,进入OTA
        {
                        printf("EraseFlash......\r\n");
                        BootLoader_EraseFlash();
                        printf("EraseFlash successfully \r\n");
                        ymodem_t.OTA_FLAG = 1;
                        ymodem_t.YMODE_FLAG = 1;
        }
}

/***************************************************************************
Ymodem数据帧:        
                帧头                  包号 包号反码         信息块                                                 校验
        SOH/STX                 PN                 XPN                         DATA 128/1024                 CRC16(CRCH CRCL)
                1:对于SOH帧,若余下数据小于128字节,则以0x1A填充,该帧长度仍为133字节。
                2:对于STX帧需考虑几种情况

帧长度:
                1:以SOH(0x01)开始的数据包,信息块是128字节,该类型帧总长度为133字节
                2:以STX(0x02)开始的数据包,信息块是1024字节,该类型帧总长度为1029字节
                        a:●余下数据等于1024字节,以1029长度帧发送;
                        b:●余下数据小于1024字节,但大于128字节,以1029字节帧长度发送,无效数据以0x1A填充
                        c:●余下数据等于128字节,以133字节帧长度发送
                        d:●余下数据小于128字节,以133字节帧长度发送,无效数据以0x1A填充

包序号:
        数据包序号只有1字节,因此计算范围是0~255;对于数据包大于255的,序号归零重复计算。

校验:
        Ymodem采用的是CRC16校验算法,校验值为2字节,传输时CRC高八位在前,低八位在后;CRC计算数据为信息块数据,不包含帧头、包号、包号反码

起始帧:
        Ymodem起始帧并不直接传输文件内容,而是先将文件名和文件大小置于数据帧中传输;起始帧是以SOH 133字节长度帧传输,格式如下
        帧头                          包号 包号反码                 文名件称                                          文件大小                                                        填充区                                 校验
        SOH/STX                PN                 XPN                                fileName+0x00                                fileSize+0x00                                                0x00                                CRC16(CRCH CRCL)

Ymodem结束帧
                帧头                  包号         包号反码                         信息块                                                 校验
                SOH                                 PN                         XPN                                  0x00                                                        CRC16(CRCH CRCL)
               
               
文件传输过程

文件的传输过程,以具体的例子说明。把foo.c,大小为4196Byte(16进制为0x1064)的文件作为传输的对象,则它的传输过程如下:
发送端----------------------------------------------------------------接收端

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< C

SOH 00 FF “foo.c” "1064’’ NUL[118] CRC CRC >>>>>>>>>>>>>

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ACK

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< C

STX 01 FE data[1024] CRC CRC>>>>>>>>>>>>>>>>>>>>>>>>

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ACK

STX 02 FD data[1024] CRC CRC>>>>>>>>>>>>>>>>>>>>>>>

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ACK

STX 03 FC data[1024] CRC CRC>>>>>>>>>>>>>>>>>>>>>>>

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ACK

STX 04 FB data[1024] CRC CRC>>>>>>>>>>>>>>>>>>>>>>>

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ACK

SOH 05 FA data[100] 1A[28] CRC CRC>>>>>>>>>>>>>>>>>>

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ACK

EOT >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< NAK

EOT>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ACK

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< C

SOH 00 FF NUL[128] CRC CRC >>>>>>>>>>>>>>>>>>>>>>>

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ACK        
*******************************************************************************************/

void Ymodem_Receive(uint8_t *data,uint16_t datalen)
{
        char *fileSizebuff;
        char fileSize[50];                        //文件大小
        uint8_t i = 0,j=0;
        
        if(ymodem_t.OTA_FLAG == YMODEM_DISABLE)
        {
                BootLoader_Com(data);
        }
        
        //当是第0包数据时,和最1后才能进入,
        if((data[0] == SOH && data[1] == 0x00 && data[2] == 0xff && ymodem_t.YMODE_Frist_Pack_FLAG == YMODEM_DISABLE && datalen == YMODE_SOH_PACKLEN) || ymodem_t.YMODE_Frist_FLAG == YMODEMD_ENABLE )        //Ymodem起始帧: 113
        {               
                        ymodem_t.crc = CRC16_XMODEM(&data[3],YMODE_SOH_DATALEN);
                        if(ymodem_t.crc == ((data[YMODE_SOH_PACKLEN - 2]<<8) |data[YMODE_SOH_PACKLEN - 1]))        //CRC
                        {
                                       
                                        if(ymodem_t.YMODE_Frist_FLAG == YMODEM_DISABLE)        //第1次
                                        {
                                                ymodem_t.YMODE_FLAG = YMODEM_DISABLE;
                                                ymodem_t.YMODE_Frist_Pack_FLAG = YMODEMD_ENABLE ;        //第1包接收标志位置使能,说时第一包接好
                                                for(i=0;data[3+i] !='\0';i++)                //读取文件名
                                                {
                                                        ymodem_t.fileName[i] = data[3+i];
                                                }
                                                i++;
                                                for(j=0;data[3+i+j] != '\0';j++)        //读取文件大小
                                                {
                                                        fileSize[j] = data[3+i+j];
                                                }
                                                
                                                        ymodem_t.fileSize = strtol(fileSize,&fileSizebuff,10);
                                                        ymodem_t.remanider =         ymodem_t.fileSize % YMODE_SOH_DATALEN;        
                                                        ymodem_t.packnum   =        ymodem_t.fileSize /        YMODE_SOH_DATALEN;
                                                        if(ymodem_t.remanider > 0)
                                                                ymodem_t.packnum = ymodem_t.packnum+1;
                                                        
                                                BootLoader_SendByte(ACK);        //正确发送ACK
                                                BootLoader_SendByte(CA);
                                        }
                                        else if(ymodem_t.YMODE_Frist_FLAG == YMODEMD_ENABLE)
                                        {
                                                
                                                BootLoader_SendByte(ACK);        //正确发送ACK
                                                
                                                BootLoader_SendByte(CA);        //正确发送ACK

                                                Delay_ms(20);
                                                printf("\r\n");
                                                printf("fileName :%s \r\n",ymodem_t.fileName);
                                                printf("fileSize :%d Byte\r\n",ymodem_t.fileSize);
                                                printf("File uploaded successfully  \r\n");
                                                printf("\r\n");
                                                Ymodem_Init();
                                                BootLoader_To_App();
                                                
                                        }
                        }
                        else{
                                BootLoader_SendByte(NAK);        //错误发送NAK
                        }
                        
        }
        else if((data[0] == STX && data[1] == 0x00 && data[2] == 0xff && ymodem_t.YMODE_Frist_Pack_FLAG == YMODEM_DISABLE && datalen == YMODE_STX_PACKLEN))        ////Ymodem起始帧: 1024
        {
                        ymodem_t.crc = CRC16_XMODEM(&data[3],YMODE_STX_DATALEN);
                        if(ymodem_t.crc == ((data[YMODE_STX_PACKLEN - 2]<<8) |data[YMODE_STX_PACKLEN - 1]))        //CRC
                        {
                                        if(ymodem_t.YMODE_Frist_FLAG == YMODEM_DISABLE)        //第1次
                                        {
                                                ymodem_t.YMODE_FLAG = YMODEM_DISABLE;
                                                ymodem_t.YMODE_Frist_Pack_FLAG = YMODEMD_ENABLE;        //第1包接收标志位置使能,说时第一包接好
                                                
                                                for(i=0;data[3+i] !='\0';i++)                //读取文件名
                                                {
                                                        ymodem_t.fileName[i] = data[3+i];
                                                }
                                                i++;
                                                for(j=0;data[3+i+j] != '\0';j++)        //读取文件大小
                                                {
                                                        fileSize[j] = data[3+i+j];
                                                }
                                                        ymodem_t.fileSize = strtol(fileSize,&fileSizebuff,10);
                                                

                                                
                                                        //数据1024时,数据是1024时,按1024接收,当小于1024时,以每包128个字传接收,直到所有数据收完
                                                        ymodem_t.remanider =         ymodem_t.fileSize % YMODE_STX_DATALEN;                //余下字节个数
                                                        
                                                        ymodem_t.remaniderpacknum = ymodem_t.remanider / YMODE_SOH_DATALEN;        //余下字节以128接收的包数据
                                                        
                                                        ymodem_t.packnum   =        (ymodem_t.fileSize /        YMODE_STX_DATALEN)  ;                //1024总包数        
                                                        if(ymodem_t.remanider  > 0)
                                                        {
                                                                ymodem_t.remaniderpacknum = ymodem_t.remaniderpacknum +1;
                                                                ymodem_t.packnum = ymodem_t.packnum+1;
                                                        }
                                                        
                                                        ymodem_t.YMODE_EOH_STX_DATALEN_FLAG = YMODEMD_ENABLE;                        
                                       
                                                BootLoader_SendByte(ACK);        //正确发送ACK
                                                BootLoader_SendByte(CA);
                                                        
                                        }        
                        }
                        else{
                                BootLoader_SendByte(NAK);        //错误发送NAK
                        }
        }
        else if(data[0] == SOH && datalen == YMODE_SOH_PACKLEN)        //Ymodem数据帧:当数据以大小是128字节时
        {                        
                        ymodem_t.crc = CRC16_XMODEM(&data[3],YMODE_SOH_DATALEN);
                        if(ymodem_t.crc == ((data[YMODE_SOH_PACKLEN - 2]<<8) |data[YMODE_SOH_PACKLEN - 1]))        //CRC
                        {
                                                ymodem_t.num ++;        //已接收的数据包数量+1
                                       
                                                //将本次接收的数据,暂存到ymodem_t.UpdataBuffer缓冲                                                
                                                        memcpy(&ymodem_t.UpdataBuffer[((ymodem_t.num - 1) % (STM32_PAGE_SIZE / YMODE_SOH_DATALEN)) * YMODE_SOH_DATALEN],&data[3], YMODE_SOH_DATALEN);   //将本次接收的数据,暂存到UpDataA.Updatabuff缓冲区
                                                if(ymodem_t.YMODE_EOH_STX_DATALEN_FLAG == YMODEM_DISABLE)        //当是128每包数据时
                                                {
                                                        if(((ymodem_t.num % (STM32_PAGE_SIZE/YMODE_SOH_DATALEN)) == 0) &&( ymodem_t.packnum!=ymodem_t.num ))        //每满一个扇区写入Flash
                                                        {                                                        
                                                                //写入到单片机A区相应的扇区
                                                                BootLoader_WriteFlash(STM32_A_SADDR + (((ymodem_t.num / (STM32_PAGE_SIZE / YMODE_SOH_DATALEN)) -1) * STM32_PAGE_SIZE), (uint32_t *)ymodem_t.UpdataBuffer,STM32_PAGE_SIZE);    //写入到单片机A区相应的扇区
                                                               
                                                                //备份FLASG写入
                                                        }
                                                        else if(((ymodem_t.num % (STM32_PAGE_SIZE/YMODE_SOH_DATALEN)) != 0) &&(ymodem_t.packnum==ymodem_t.num))//最后一包数,判断是否还有不满1扇区1024字节的数据,如果有进入if,把剩余的小尾巴写入
                                                        {
                                                                //写入到单片机A区相应的扇区
                                                                BootLoader_WriteFlash(STM32_A_SADDR  + (((ymodem_t.num / (STM32_PAGE_SIZE / YMODE_SOH_DATALEN))) * STM32_PAGE_SIZE), (uint32_t *)ymodem_t.UpdataBuffer,(ymodem_t.num % (STM32_PAGE_SIZE / YMODE_SOH_DATALEN)) * YMODE_SOH_DATALEN);    //写入到单片机A区相应的扇区
                                                               
                                                                //备份FLASG写入        
                                                        }
                                                }
                                                else {
                                                        if(((ymodem_t.num % (STM32_PAGE_SIZE/YMODE_SOH_DATALEN)) == 0) &&( ymodem_t.remaniderpacknum!=ymodem_t.num ))        //每满一个扇区写入Flash
                                                        {                                                        
                                                                //写入到单片机A区相应的扇区
                                                                BootLoader_WriteFlash(STM32_A_SADDR + (ymodem_t.STXnum * STM32_PAGE_SIZE)+(((ymodem_t.num / (STM32_PAGE_SIZE / YMODE_SOH_DATALEN)) -1) * STM32_PAGE_SIZE), (uint32_t *)ymodem_t.UpdataBuffer,STM32_PAGE_SIZE);    //写入到单片机A区相应的扇区
                                                               
                                                                //备份FLASG写入
                                                        }        
                                                        else if(((ymodem_t.num % (STM32_PAGE_SIZE/YMODE_SOH_DATALEN)) != 0) &&(ymodem_t.remaniderpacknum==ymodem_t.num))//最后一包数,判断是否还有不满1扇区1024字节的数据,如果有进入if,把剩余的小尾巴写入
                                                        {
                                                                //写入到单片机A区相应的扇区
                                                                BootLoader_WriteFlash(STM32_A_SADDR + (ymodem_t.STXnum * STM32_PAGE_SIZE) + (((ymodem_t.num / (STM32_PAGE_SIZE / YMODE_SOH_DATALEN))) * STM32_PAGE_SIZE), (uint32_t *)ymodem_t.UpdataBuffer,(ymodem_t.num % (STM32_PAGE_SIZE / YMODE_SOH_DATALEN)) * YMODE_SOH_DATALEN);    //写入到单片机A区相应的扇区                                                
                                                               
                                                                //备份FLASG写入        
                                                        }                                                        
                                                }

                                                BootLoader_SendByte(ACK);        //正确发送ACK
                                                        
                        }
                        else{
                                BootLoader_SendByte(NAK);        //错误发送NAK
                        }
        }
        else if(data[0] == STX && datalen == YMODE_STX_PACKLEN)        //Ymodem数据帧:当数据是1029字节时
        {
                        ymodem_t.crc = CRC16_XMODEM(&data[3],YMODE_STX_DATALEN);
                        if(ymodem_t.crc == ((data[YMODE_STX_PACKLEN - 2]<<8) |data[YMODE_STX_PACKLEN - 1]))        //CRC
                        {
                                ymodem_t.STXnum++;
                                memcpy(&ymodem_t.UpdataBuffer[((ymodem_t.STXnum- 1) % (STM32_PAGE_SIZE / YMODE_STX_DATALEN)) * YMODE_STX_DATALEN],&data[3], YMODE_STX_DATALEN);   //将本次接收的数据,暂存到UpDataA.Updatabuff缓冲区
                                if(((ymodem_t.STXnum % (STM32_PAGE_SIZE/YMODE_STX_DATALEN)) == 0) &&( ymodem_t.packnum!=ymodem_t.STXnum))        //每满一个扇区写入Flash
                                {                                                        
                                        //写入到单片机A区相应的扇区
                                        BootLoader_WriteFlash(STM32_A_SADDR + (((ymodem_t.STXnum/ (STM32_PAGE_SIZE / YMODE_STX_DATALEN)) -1) * STM32_PAGE_SIZE), (uint32_t *)ymodem_t.UpdataBuffer,STM32_PAGE_SIZE);    //写入到单片机A区相应的扇区
                                       
                                        //备份FLASG写入
                                       
                                }
                                
                                BootLoader_SendByte(ACK);        //正确发送ACK
                        }
                        else{
                                BootLoader_SendByte(NAK);        //错误发送NAK
                        }
               
        }
        else if(data[0] == EOT && datalen == YMODE_EOT_DATALEN)        //当接收到EOT
        {        
                if(ymodem_t.count == 0)
                {
                        ymodem_t.count ++;
                        BootLoader_SendByte(NAK);        //正确发送NAK

                }
                else if(ymodem_t.count == 1)
                {
                        ymodem_t.YMODE_Frist_FLAG = YMODEMD_ENABLE;
                        ymodem_t.count = 0;
                        BootLoader_SendByte(ACK);        //正确发送ACK                        
                        BootLoader_SendByte(CA);        //正确发送C        

                }
        }
        else if(data[0] == CAN && data[1] == CAN && data[2] == CAN && data[3] == CAN && data[4] == CAN )        //取消传输命令,连续发送5个该命令
        {
                        ymodem_t.OTA_FLAG = YMODEM_DISABLE;
                        Ymodem_Init();
        }
}


#ifndef __YMODEM_H
#define __YMODEM_H

#include "stm32f4xx.h"

#define  STM32_FLASH_SADDR   0x08000000                                                              //FLASH扇区起始地址
#define  STM32_PAGE_SIZE     1024                                                                    //FLASH扇区大小
#define  STM32_PAGE_NUM      1024                                                                     //FLASH扇区总个数
#define  STM32_B_PAGE_NUM    16                                                                      //BootLoader 扇区个数

#define  STM32_A_PAGE_NUM    (STM32_PAGE_NUM - STM32_B_PAGE_NUM)                                                                                                         //APP扇区个数
#define  STM32_A_START_PAGE  (STM32_B_PAGE_NUM)                                                                                                                                                                                //APP扇区起始数
#define  STM32_A_SADDR       (STM32_FLASH_SADDR + STM32_A_START_PAGE * STM32_PAGE_SIZE)                //APP扇区起始地址

#define APPLICATION_ADDRESS (STM32_A_SADDR)                                                                                                                                                                                                //APP跳转地址


#define YMODE_SOH_PACKLEN 133                        //SOH数据包长度
#define YMODE_STX_PACKLEN 1029                //SOH数据包长度        
#define YMODE_SOH_DATALEN 128                        //SOH有效数据长度
#define YMODE_STX_DATALEN 1024                //SOH有效数据长度        
#define YMODE_EOT_DATALEN   1

typedef enum {YMODEM_DISABLE = 0, YMODEMD_ENABLE = !YMODEM_DISABLE} YmodemState;

typedef enum{
        SOH = 0X01,//133字节长度帧
        STX = 0X02,//1024字节长度帧
        EOT = 0X04,//文件传输结束命令
        ACK = 0X06,//接收正确应答命令
        NAK = 0X15,//重传当前数据包请求命令
        CAN = 0X18,//取消传输命令,连续发送5个该命令
        CA   = 0X43,//字符C ,发送握手        
}YMODEM_COM;

void Ymodem_Init(void);
void BootLoader_Meun(void);
void Ymodem_Receive(uint8_t *data,uint16_t datalen);
#endif

usart.c
#include "usart.h"
#include "delay.h"
uint8_t Usart1_RxBuffer[UARTx_RX_SIZE] = {0};      //串口1接收缓冲区
uint8_t Usart1_TxBuffer[UARTx_TX_SIZE] = {0};      //串口1发送缓冲区
Usartx_Control_Block  usart1;                                  //串口1控制结构体

void (*usartTakeFunCb)(uint8_t *data,uint16_t datalen);

void usart1TakeCb(void(*pFunc)(uint8_t *data,uint16_t datalen))
{
        usartTakeFunCb = pFunc;
}

void usart1_take(void)
{
        if(usart1.RxOutPtr != usart1.RxInPtr)
        {
                usartTakeFunCb(usart1.RxOutPtr->start,(usart1.RxOutPtr->end - usart1.RxOutPtr->start+1));
               
                usart1.RxOutPtr++;
                if(usart1.RxOutPtr == usart1.RxEndPtr)
                {
                        usart1.RxOutPtr = &usart1.RxLocation[0];
                }
        }

        if((usart1.TxOutPtr != usart1.TxInPtr) &&( usart1.TxState == 0))
        {
                usart1.TxState = 1;
               
                DMA_Cmd(DMA2_Stream7,ENABLE);       
                while (DMA_GetCmdStatus(DMA2_Stream7) != DISABLE);        //确保DMA可以被设置
                DMA_SetCurrDataCounter(DMA2_Stream7, UARTx_TX_MAX);               
                usart1.TxOutPtr++;
                if(usart1.TxOutPtr == usart1.TxEndPtr)
                {
                        usart1.TxOutPtr = &usart1.TxLocation[0];
                }
       
        }       
}

void USART1_Init(uint32_t bandRate)
{
        Usart1_PtrInit();
        USART1_GPIO_Init();
        Delay_ms(1);
        USART1_Config_Init(bandRate);
        Delay_ms(1);
        USART1_DMA_Init();         
}

/*************************************************************************
*        函 数 名: Uart1_RX_PtrInit
*        功能说明: 串口1控制结构体各个指针初始化
*        形    参:无
*        返 回 值: 无
**************************************************************************/
void Usart1_PtrInit(void)
{       
        usart1.RxInPtr  = &usart1.RxLocation[0];
        usart1.RxOutPtr = &usart1.RxLocation[0];
        usart1.RxEndPtr = &usart1.RxLocation[9];
        usart1.RxCounter = 0;
        usart1.RxInPtr->start = Usart1_RxBuffer;

        usart1.TxInPtr   = &usart1.TxLocation[0];
        usart1.TxOutPtr  = &usart1.TxLocation[0];
        usart1.TxEndPtr  = &usart1.TxLocation[9];
        usart1.TxCounter = 0;
        usart1.TxInPtr->start = Usart1_TxBuffer;
       
        memset(Usart1_RxBuffer,0,UARTx_RX_SIZE);
        memset(Usart1_TxBuffer,0,UARTx_RX_SIZE);
}

/*************************************************************************
*        函 数 名: Usart1_GPIO_Config
*        功能说明: 串口1 GPIO初始化
*        形    参:无
*        返 回 值: 无
**************************************************************************/

void USART1_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStructure;
               
  RCC_AHB1PeriphClockCmd( RCC_AHB1Periph_GPIOB, ENABLE);
       
  /* 配置Tx引脚为复用功能  */
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
  GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
  GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;       
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;        
  GPIO_Init(GPIOB, &GPIO_InitStructure);
       
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7  ;
  GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;       
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
  GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;       
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;           
  GPIO_Init(GPIOB, &GPIO_InitStructure);       
       
       
        /*串口上电发送0x00的解决*/
  /* 连接 PXx 到 USARTx_Tx*/
        GPIO_PinAFConfig(GPIOB,GPIO_PinSource6, GPIO_AF_USART1);

        /*  连接 PXx 到 USARTx__Rx*/
        GPIO_PinAFConfig(GPIOB,GPIO_PinSource7,GPIO_AF_USART1);               
}

/*************************************************************************
*        函 数 名: Usart1_Config
*        功能说明: 串口1初始化
*        形    参:bandRate:波特率
*        返 回 值: 无
**************************************************************************/
void USART1_Config_Init(uint32_t bandRate)
{

  /* 使能 UART 时钟 */
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
        usart1.usart.USART_BaudRate                       = bandRate;
        usart1.usart.USART_WordLength                   = USART_WordLength_8b;                     //8bit数据位
        usart1.usart.USART_StopBits                           = USART_StopBits_1;                        //1bit停止位
        usart1.usart.USART_Parity                           =          USART_Parity_No;                         //无校验
        usart1.usart.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
        usart1.usart.USART_Mode                               = USART_Mode_Rx | USART_Mode_Tx;      
        USART_Init(USART1, &usart1.usart);       

//        USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);        //开启空闲中断
        USART_ITConfig(USART1,USART_IT_IDLE,ENABLE);        //开启空闲中断
//        USART_ITConfig(USART1,USART_IT_TC,ENABLE);        //开启发送完成中断       
        USART_Cmd(USART1, ENABLE);                                                                                                                                //使能串口       
       
        USART_DMACmd(USART1,USART_DMAReq_Rx, ENABLE);                                                                        //使能RX DMA
        USART_DMACmd(USART1,USART_DMAReq_Tx, ENABLE);                                                                        //使能RX DMA               
}

/*************************************************************************
*        函 数 名: Uart1_DMA_Init
*        功能说明: 串口1 DMA初始化
*        形    参:无
*        返 回 值: 无
**************************************************************************/
void USART1_DMA_Init(void)
{
       
        NVIC_InitTypeDef NVIC_InitStructure;
       
        RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2,ENABLE);
  /* 复位初始化DMA数据流 */
  DMA_DeInit(DMA2_Stream5);

  /* 确保DMA数据流复位完成 */
  while (DMA_GetCmdStatus(DMA2_Stream5) != DISABLE)  {
  }

  /*usart1 rx对应dma2,通道4,数据流2*/       
  usart1.dmarx.DMA_Channel = DMA_Channel_4;  
  /*设置DMA源:串口数据寄存器地址*/
  usart1.dmarx.DMA_PeripheralBaseAddr =  (USART1_BASE + 0x04);         
  /*内存地址(要传输的变量的指针)*/
  usart1.dmarx.DMA_Memory0BaseAddr = (uint32_t)Usart1_RxBuffer;
  /*方向:从内存到外设*/               
  usart1.dmarx.DMA_DIR = DMA_DIR_PeripheralToMemory;       
  /*传输大小DMA_BufferSize=RECEIVEBUFF_SIZE*/       
  usart1.dmarx.DMA_BufferSize = UARTx_RX_MAX+1;
  /*外设地址不增*/            
  usart1.dmarx.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
  /*内存地址自增*/
  usart1.dmarx.DMA_MemoryInc = DMA_MemoryInc_Enable;       
  /*外设数据单位*/       
  usart1.dmarx.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
  /*内存数据单位 8bit*/
  usart1.dmarx.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;       
  /*DMA模式:不断循环*/
  usart1.dmarx.DMA_Mode = DMA_Mode_Circular;         
  /*优先级:中*/       
  usart1.dmarx.DMA_Priority = DMA_Priority_Medium;      
  /*禁用FIFO*/
  usart1.dmarx.DMA_FIFOMode = DMA_FIFOMode_Disable;        
  usart1.dmarx.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;   
  /*存储器突发传输 16个节拍*/
  usart1.dmarx.DMA_MemoryBurst = DMA_MemoryBurst_Single;   
  /*外设突发传输 1个节拍*/
  usart1.dmarx.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;   
  /*配置DMA2的数据流2*/                  
  DMA_Init(DMA2_Stream5, &usart1.dmarx);
  
//  /*使能DMA*/
  DMA_Cmd(DMA2_Stream5, ENABLE);
  
  /* 等待DMA数据流有效*/
  while(DMA_GetCmdStatus(DMA2_Stream5) != ENABLE)
  {
  }

  /* 复位初始化DMA数据流 */
  DMA_DeInit(DMA2_Stream7);

  /* 确保DMA数据流复位完成 */
  while (DMA_GetCmdStatus(DMA2_Stream7) != DISABLE)  {
  }

  /*usart1 tx对应dma2,通道4,数据流7*/       
  usart1.dmatx.DMA_Channel = DMA_Channel_4;  
  /*设置DMA源:串口数据寄存器地址*/
  usart1.dmatx.DMA_PeripheralBaseAddr =  (USART1_BASE+0x04);         
  /*内存地址(要传输的变量的指针)*/
  usart1.dmatx.DMA_Memory0BaseAddr = (uint32_t)Usart1_TxBuffer;
  /*方向:从内存到外设*/               
  usart1.dmatx.DMA_DIR = DMA_DIR_MemoryToPeripheral;       
  /*传输大小DMA_BufferSize=SENDBUFF_SIZE*/       
  usart1.dmatx.DMA_BufferSize = UARTx_TX_MAX;
  /*外设地址不增*/            
  usart1.dmatx.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
  /*内存地址自增*/
  usart1.dmatx.DMA_MemoryInc = DMA_MemoryInc_Enable;       
  /*外设数据单位*/       
  usart1.dmatx.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
  /*内存数据单位 8bit*/
  usart1.dmatx.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;       
  /*DMA模式:不断循环*/
  usart1.dmatx.DMA_Mode = DMA_Mode_Circular;         
  /*优先级:中*/       
  usart1.dmatx.DMA_Priority = DMA_Priority_Medium;      
  /*禁用FIFO*/
  usart1.dmatx.DMA_FIFOMode = DMA_FIFOMode_Disable;        
  usart1.dmatx.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;   
  /*存储器突发传输 16个节拍*/
  usart1.dmatx.DMA_MemoryBurst = DMA_MemoryBurst_Single;   
  /*外设突发传输 1个节拍*/
  usart1.dmatx.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;   
  /*配置DMA2的数据流7*/                  
  DMA_Init(DMA2_Stream7, &usart1.dmatx);
//  
//  /*使能DMA*/
//  DMA_Cmd(DMA2_Stream7, ENABLE);
//  
//  /* 等待DMA数据流有效*/
//  while(DMA_GetCmdStatus(DMA2_Stream7) != ENABLE)
//  {
//  }


        DMA_ITConfig(DMA2_Stream5,DMA_IT_TC,ENABLE);
        DMA_ITConfig(DMA2_Stream7,DMA_IT_TC,ENABLE);
        // NVIC  
        NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;  
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;  
        NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;  
        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;  
        NVIC_Init(&NVIC_InitStructure);  

        NVIC_InitStructure.NVIC_IRQChannel = DMA2_Stream7_IRQn;  
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;  
        NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;  
        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;  
        NVIC_Init(&NVIC_InitStructure);   
  
        NVIC_InitStructure.NVIC_IRQChannel = DMA2_Stream5_IRQn;  
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;  
        NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;  
        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;  
        NVIC_Init(&NVIC_InitStructure);  
}

void Usart1_Txdata(uint8_t *tdata,uint16_t datalen)
{
        if(datalen == 0)
        {
                        DMA_SetCurrDataCounter(DMA2_Stream7, UARTx_TX_MAX);       
                        return;
        }
        if((UARTx_TX_SIZE - usart1.TxCounter) >= datalen) //发送空间大于等于发数据
        {
                usart1.TxInPtr ->start = &Usart1_TxBuffer[usart1.TxCounter];
        }else
        {
                usart1.TxCounter = 0;        //发送空间小于发送数据,清0
                usart1.TxInPtr->start = Usart1_TxBuffer;
        }
       
       
        while (DMA_GetCmdStatus(DMA2_Stream7) != DISABLE);        //确保DMA可以被设置
        DMA_SetCurrDataCounter(DMA2_Stream7,datalen);                        //设置数据传输长度
       
        memcpy(usart1.TxInPtr ->start,tdata,datalen);        //数据拷贝
       
        usart1.TxCounter += datalen;        //统计每次发送量
        usart1.TxInPtr ->end = &Usart1_TxBuffer[usart1.TxCounter-1];//标记end位,为下次作准备
        DMA2_Stream7->M0AR  = (uint32_t)usart1.TxInPtr->start;

        usart1.TxInPtr++;
        if(usart1.TxInPtr == usart1.TxEndPtr){
                usart1.TxInPtr = &usart1.TxLocation[0];               
        }
}

void Usartx_SendArray( USART_TypeDef * pUSARTx, uint8_t *data, uint16_t datalen)
{
        uint16_t i;
        for(i=0;i<datalen;i++)
        {
            /* 发送一个字节数据到USART */
            pUSARTx->DR = data[i];               
                while(!(pUSARTx->SR & USART_FLAG_TXE) );
        }
        while(!USART_GetFlagStatus(pUSARTx,USART_FLAG_TC));
}

void Usartx_SendStr( USART_TypeDef * pUSARTx, const char *SendStr)
{
        while(*SendStr != '\0')
        {
            /* 发送一个字节数据到USART */
          // pUSARTx->DR = *SendStr;               
                USART_SendData(pUSARTx,*SendStr);
                //while(!USART_GetFlagStatus(pUSARTx,USART_FLAG_TXE));
                while(!(pUSARTx->SR & USART_FLAG_TXE) );
                SendStr++;
        }
        while(!USART_GetFlagStatus(pUSARTx,USART_FLAG_TC));
}

//uint8_t tempbuff[256];
//void u1_printf(char *fmt, ...)
//{


//        uint16_t i=0;       

//        va_list ap;
//        va_start(ap,fmt);
//       
//        vsprintf((char *)tempbuff,fmt,ap);
//       
//        va_end(ap);
//        DMA_Cmd(DMA2_Stream7,DISABLE);
//        for(i=0;i<strlen((char *)tempbuff);i++)
//        {
//                while(!USART_GetFlagStatus(USART1,USART_FLAG_TXE));
//                USART1->DR = tempbuff[i];
//        }
//        //while(!USART_GetFlagStatus(USART1,USART_FLAG_TC));
//}



void USART1_IRQHandler(void)
{
        //发送中断
        if(USART_GetITStatus(USART1,USART_IT_TC) != RESET)
        {
                USART_ClearITPendingBit(USART1,USART_IT_TC);
                //usart1.TxState = 0;
        }                               
        //空闲中断
        if(USART_GetITStatus(USART1,USART_IT_IDLE) != RESET)
        {               
               
                USART1->SR;                       //清中断
                USART1->DR;                       //清中断       
                       
                usart1.RxCounter +=((UARTx_RX_MAX+1)- DMA_GetCurrDataCounter(DMA2_Stream5));
                usart1.RxInPtr->end = &Usart1_RxBuffer[usart1.RxCounter - 1];
               
                usart1.RxInPtr++;
                if(usart1.RxInPtr == usart1.RxEndPtr)
                {
                        usart1.RxInPtr = &usart1.RxLocation[0];
                }
                if((UARTx_RX_SIZE - usart1.RxCounter) >= UARTx_RX_MAX){
                        usart1.RxInPtr->start = &Usart1_RxBuffer[usart1.RxCounter];         //标记接位置
                                                                               
                }else{
                        usart1.RxInPtr->start = Usart1_RxBuffer;
                        usart1.RxCounter = 0;       
                }               
               
                DMA_Cmd(DMA2_Stream5, DISABLE);     //关闭DMA
                DMA_SetCurrDataCounter(DMA2_Stream5, UARTx_RX_MAX+1);
                DMA2_Stream5->M0AR = (uint32_t)usart1.RxInPtr->start;
                DMA_Cmd(DMA2_Stream5, ENABLE);     //打开DMA                       
        }       
}

void DMA2_Stream7_IRQHandler(void)
{
        if(DMA_GetITStatus(DMA2_Stream7,DMA_IT_TCIF7)!=RESET)//等待DMA2_Steam7传输完成
        {               
                DMA_Cmd(DMA2_Stream7,DISABLE);                                //关闭使能
                DMA_ClearITPendingBit(DMA2_Stream7,DMA_FLAG_TCIF7); //清除DMA2_Steam7传输完成标志       
               
                DMA_SetCurrDataCounter(DMA2_Stream7, UARTx_TX_MAX);               
                usart1.TxState = 0;
        }
        if(DMA_GetITStatus(DMA2_Stream7,DMA_IT_HTIF7)!=RESET)
        {
                DMA_ClearITPendingBit(DMA2_Stream7,DMA_IT_HTIF7);
        }

}

void DMA2_Stream5_IRQHandler(void)
{
        if(DMA_GetITStatus(DMA2_Stream5,DMA_IT_TCIF5)!=RESET)//等待DMA2_Steam2传输完成
        {
                DMA_ClearITPendingBit(DMA2_Stream5,DMA_FLAG_TCIF5); //清除DMA2_Steam2传输完成标志       
                DMA_Cmd(DMA2_Stream5, ENABLE);   
        }
        if(DMA_GetITStatus(DMA2_Stream5,DMA_IT_HTIF5)!=RESET)
        {
                DMA_ClearITPendingBit(DMA2_Stream5,DMA_FLAG_TCIF7);
        }       

}

/* 加入以下代码, 支持printf函数, 而不需要选择use MicroLIB */

#if 1
#if (__ARMCC_VERSION >= 6010050)                    /* 使用AC6编译器时 */
__asm(".global __use_no_semihosting\n\t");          /* 声明不使用半主机模式 */
__asm(".global __ARM_use_no_argv \n\t");            /* AC6下需要声明main函数为无参数格式,否则部分例程可能出现半主机模式 */

#else
/* 使用AC5编译器时, 要在这里定义__FILE 和 不使用半主机模式 */
#pragma import(__use_no_semihosting)

struct __FILE
{
    int handle;
    /* Whatever you require here. If the only file you are using is */
    /* standard output using printf() for debugging, no file handling */
    /* is required. */
};

#endif

/* 不使用半主机模式,至少需要重定义_ttywrch\_sys_exit\_sys_command_string函数,以同时兼容AC6和AC5模式 */
int _ttywrch(int ch)
{
    ch = ch;
    return ch;
}

/* 定义_sys_exit()以避免使用半主机模式 */
void _sys_exit(int x)
{
    x = x;
}

char *_sys_command_string(char *cmd, int len)
{
    return NULL;
}

/* FILE 在 stdio.h里面定义. */
FILE __stdout;

/* 重定义fputc函数, printf函数最终会通过调用fputc输出字符串到串口 */
int fputc(int ch, FILE *f)
{
    while ((USART1->SR & 0X40) == 0);               /* 等待上一个字符发送完成 */

    USART1->DR = (uint8_t)ch;                       /* 将要发送的字符 ch 写入到DR寄存器 */
       
       
    return ch;
}

#endif
/***********************************************END*******************************************/

usart.h
#ifndef __USART_H
#define __USART_H
#include "stm32f4xx.h"

#include <stdint.h>
#include "string.h"
#include "signal.h"
#include "stdarg.h"
#include "stdio.h"
#include "stdlib.h"
#include <stdbool.h>

#define UARTx_RX_SIZE   4096                 //接收缓冲区长度
#define UARTx_TX_SIZE   4096                //发送缓冲区长度
#define UARTx_RX_MAX    1029                        //单次接收最大量
#define UARTx_TX_MAX    256                        //单次接收最大量
#define SE_PTR_NUM      10                        //se指针对结构体数组长度


typedef struct{
    uint8_t *start;                                         //start用于标记起始位置                                                                       
    uint8_t *end;                                        //end用于标记结束位置
}LCB;                        //se 指针对结构体

typedef struct{
       
       
        uint16_t RxCounter;        //累计接收数据量
        uint16_t TxCounter;
        uint16_t TxState;
        LCB RxLocation[SE_PTR_NUM];        //se指针对结构体数组
        LCB *RxInPtr;                        //IN指针用于标记接收数据
        LCB *RxOutPtr;                        //OUT指针用于提取接收的数据
        LCB *RxEndPtr;                        ////IN和OUT指针的结尾标志
       
        LCB TxLocation[SE_PTR_NUM];        //se指针对结构体数组
        LCB *TxInPtr;                        //IN指针用于标记发送数据
        LCB *TxOutPtr;                        //OUT指针用于提取发送的数据
        LCB *TxEndPtr;                        ////IN和OUT指针的结尾标志

        USART_InitTypeDef usart;
        DMA_InitTypeDef  dmatx;       
        DMA_InitTypeDef  dmarx;
       
}Usartx_Control_Block;

extern uint8_t Usart1_RxBuffer[UARTx_RX_SIZE];      //串口1接收缓冲区
extern uint8_t Usart1_TxBuffer[UARTx_TX_SIZE];      //串口1发送缓冲区
extern Usartx_Control_Block  usart1;                                                               //串口控制结构体


  

void USART1_Init(uint32_t bandRate);
void USART1_GPIO_Init(void);
void USART1_Config_Init(uint32_t bandRate);
void USART1_DMA_Init(void);
void Usart1_PtrInit(void);
void Usart1_Txdata(uint8_t *tdata,uint16_t datalen);

//void u1_printf(char *fmt, ...);
void Usart1_Rx_Data(uint8_t *rdata);
void usart1TakeCb(void(*pFunc)(uint8_t *data,uint16_t datalen));

void usart1_take(void);


void Clearsart3_RxBuffer(void);

void Usartx_SendStr( USART_TypeDef * pUSARTx, const char *SendStr);
void Usartx_SendArray( USART_TypeDef * pUSARTx, uint8_t *data, uint16_t datalen);
#endif







使用特权

评论回复
沙发
zhengshuai888| | 2024-4-21 19:25 | 只看该作者
Y我晕,还要付费才能看啊。

使用特权

评论回复
板凳
WoodData| | 2024-4-21 22:25 | 只看该作者
这种IAP都很简单了

使用特权

评论回复
地板
zero949079783|  楼主 | 2024-4-21 23:14 | 只看该作者
WoodData 发表于 2024-4-21 22:25
这种IAP都很简单了

简单但是烦而已

使用特权

评论回复
5
lidi911| | 2024-4-22 08:35 | 只看该作者
除了YModem还有XModem和ZModem

使用特权

评论回复
6
cmyldd| | 2024-5-11 10:01 | 只看该作者
协议是有许多种的, 原理差不多都一样,用熟哪种就那种

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

32

主题

84

帖子

1

粉丝