FreeRTOS-Qemu 实现三任务同步通信机制以及API信息


1. 本次作业的考察要点:

作业地址:https://github.com/HustWolfzzb/STM32F429DiscoveryFreeRTOS_9Git/GCC/GDB/QEMU等工具的使用。 FreeRTOS多任务同步和通信机制的掌握。


2. 编程作业:

在github上,Fork例程项目(https://github.com/cbhust/STM32F429DiscoveryFreeRTOS_9.git) 到自己的个人账号。 clone项目到本地电脑的Ubuntu虚拟机中(虚拟机环境在第一次作业中已搭建)。 按照/Projects/Demo1/README.md中的提示编译Demo1例程并通过Qemu运行例程。 在Demo1的框架基础上完成自己的本次编程作业(具体要求见第3点)。 代码完成后提交到自己的github账号上,确保助教可以正常的clone并编译运行。 在作业博客上给出代码的github链接、代码说明以及运行结果展示。


3. 编程作业具体要求:

创建三个任务:SenderTask,ReceiverTask, Monitor_Task

  • SenderTask的任务执行周期为2ms,ReceiverTask的任务执行周期为1000ms, Monitor_Task的任务执行周期为10000ms。
  • SenderTask在每个执行周期向ReceiverTask发送一个32位无符号整数,第一次发送1,然后依次发送2,3,4......,发送完10000后再从1开始发送。同时对发送的数据进行计算累加计算并保存当前累加结果。
  • ReceiverTask对接收到的数据进行和SenderTask同样的累加计算并保存当前累加结果。
  • MonitorTask在每个执行周期检查SenderTask发送的每个数据是否都被Receiver_Task正确的接收和处理,请自行设计一种检查机制并实现。
  • 可利用STM32F429I Discovery开发板的相关硬件(LED/LCD/串口)来输出相关状态信息。
  • 使用FreeRTOS的任务间通信和同步API完成上述功能。

4. 任务说明(会用到的API)

  • 任务创建 头文件:task.h portBASETYPE xTaskCreate ( pdTASKCODE pvTaskCode, 指向任务的实现函数的指针。效果上仅仅是函数名 const portCHAR * const pcNane, 具有描述性的任务名。FreeRTOS 不会使用它。 unsigned portSHORT usStackDepth, 指定任务堆栈的大小 void *pvParameters, 指针用于作为一个参数传向创建的任务 unsigned portBASE_TYPE uxPriority, 任务运行时的优先级 xTaskHandle *pvCreatedTask 用于传递任务的句柄,可以引用从而对任务进行其他操作。 )

说明:

  1. 这里的任务是指一个永远不会退出的C 函数,通常是一个死循环。
  2. pcNane 其只是单纯地用于辅助调试。应用程序可以通过定义常量 configMAXTASKNAMELEN 来定义任务名的最大长度——包括’\0’结束符。如果传入的 字符串长度超过了这个最大值,字符串将会自动被截断
  3. usStackDepth 这个值指定的是栈空间可以保存多少个字(word),而不是多少个字节(byte)。栈空间 大小为usStackDepth*4(bytes)。
  4. uxPriority 优先级的取值范围可以从最低优先级0 到最高优先级(configMAX_PRIORITIES–1)。 返回:
  5. pdPASS 表明任务创建成功,准备运行。
  6. errCOULDNOTALLOCATEREQUIREDMEMORY 由于内存堆空间不足,FreeRTOS 无法分配 足够的空间来保存任务结构数据和任务栈,因此无法创建任务。
  • 任务延时 头文件:task.h void vTaskDelay ( portTickType xTicksToDelay 时间数量,调用任务应该锁住的时间片周期 )

说明:

  1. FreeRTOSConfig.h 中的INCLUDE_vTaskDelay=1,这个函数才能用。
  2. 延时任务为已知时间片,任务被锁住剩余的实际时间由时间片速率决定。portTICKRATEMS 常量 以时间片速率来计算实际时间
  3. vTaskDelay()指定一个任务希望的时间段,这个时间之后任务解锁。
  4. vTaskDelay()不提供一个控制周期性任务频率的好方法,和其他任务和中断一样,在调用vTaskDelay()
  • 创建一个新的队列 头文件:queue. H xQueueHandle xQueueCreate ( unsigned portBASETYPE uxQueueLength, 队列中包含最大项目数量 unsigned portBASETYPE uxItemSize 队列中每个项目所需的字节数 );

说明:创建一个新的队列。为新的队列分配所需的存储内存,并返回一个队列处理。 注意:项目通过复制而不是引用排队,因此,所需的字节数,将复制给每个项目。队列中每个项目必须分配同样大小。 返回:如果队列成功创建,则返回一个新建队列的处理。如果不能创建队列,将返回0。

  • 传递项目到一个队列中的后面 头文件:queue. H portBASE_TYPE xQueueSendToBack ( xQueueHandle xQueue, 将项目传进的队列 const void * pvItemToQueue, 项目的指针【源数据】 portTickType xTicksToWait 等待的最大时间量 );

说明:这个与xQueueSend 是一样的,参照xQueueSend 的用法

  • 从队列接收一个项目 头文件:queue. H portBASE_TYPE xQueueReceive ( xQueueHandle xQueue, 发送项目的队列句柄 void *pvBuffer, 指向缓冲区的指针,将接收的项目被复制进去 portTickType xTicksToWait 任务中断并等待队列中可用空间的最大时间 );

说明:这个项目通过复制接收,因此缓冲器必须提供足够大的空间。这个函数一定不能在中断服务程序中使用当队列空时,肯定复制传递不成功,则等待xTicksToWait 个滴答周期后再复制,但如果xTicksToWait 设置为0,调用将立即返回。 返回:如果项目成功被队列接收为pdTRUE ,否则为 pdFALSE。


~~ ~~ ~~~~~ ~~~~~~~ # 作业过程(多图预警) ~ ~~~ ~~ ~~ ~~ ~~~~ ~


1. QEMU安装

本例程使用qemu-system-gnuarmeclipse,该qemu分支对stm32f4有更好的支持,主要面向Eclipse开发环境,本文档给出在Ubuntu 16.04命令行环境下单独使用的方式。

  • 到网页 https://github.com/gnu-mcu-eclipse/qemu/releases/tag/gae-2.8.0-20161227 下载二进制文件 gnuarmeclipse-qemu-debian64-2.8.0-201612271623-dev.tgz到~/work目录
  • 在工作目录解压缩文件,并把路径添加到$PATH变量中
#cd ~/work
#tar xvf gnuarmeclipse-qemu-debian64-2.8.0-201612271623-dev.tgz
#chmod -R -w ./qemu
export PATH=~/work/qemu/2.8.0-201612271623-dev/bin/:$PATH
  • 测试qemu能否正常运行
#qemu-system-gnuarmeclipse --version

如正常则会显示版本信息为2.8.0。

2. 编译本例程

  • 在Demo1目录下运行make,生成hello_rtos.elf文件

说明:qemu-system-gnuarmeclipse当前版本不支持STM32F4的浮点,相应的,FreeRTOS使用的portable目录没有使用ARMCM4F而是使用ARMCM3。

3. QEMU仿真

在Demo1目录下提供了一个qemu.sh脚本文件,内容如下:

#!/bin/bash

qemu-system-gnuarmeclipse --verbose --verbose --board STM32F429I-Discovery --mcu STM32F429ZI -d unimp,guest_errors  --image hello_rtos.elf --semihosting-config enable

在Demo1目录下运行脚本文件:

#./qemu.sh

则qemu开始执行hello_rtos.elf文件,在弹出的GUI中看到程序执行的效果。

4. GDB调试

Ubuntu缺省安装中没有gdb-arm-none-eabi工具,需要先安装

#sudo apt-get install gdb-arm-none-eabi

在Demo1目录下运行qemu_gdb脚本文件,该文件中添加了--gdb tcp::1234 -S

qemu启动后等待来自gdb的调试指令,打开另外一个终端窗口,运行

#arm-none-eabi-gdb

在gdb界面内运行:

(gdb)target remote localhost:1234
(gdb)continue

可以看到qemu开始执行程序,GUI界面上可以看到程序执行效果。

## 5. Trace(略)

本例程启动了FreeRTOS的trace功能。

  • 用STM32 ST-LINK Utility把hellortos.hex烧写入STM32F429I Discovery开发板,程序运行时,用STM32 ST-LINK Utility读取0x20000000到0x20030000范围内的地址数据,并保存到一个二进制文件tracedata.bin中。
  • 到https://percepio.com/tz/freertostrace/ 下载并安装Tracealyzer for FreeRTOS软件。
  • 在Tracealyzer for FreeRTOS软件中打开trace_data.bin,可以看到详细的FreeRTOS的任务和事件信息。

6.自主编程实现三个同步通信的函数:SenderTask,ReceiverTask,Monitor_Task;

我采用了Mac编程,然后通过git的方式同步到ubuntu,不仅锻炼了团队写作的神器--“Git”的使用技巧,同时也优化了自己的编程环境

说一下我的思路: 首先,采用全局变量来统领发送的数据和接收到的数据,通过在main函数外声明了一个发送数据的和,一个接收数据的和,一个队列用于传递由于周期不对等的发送和接受的数据。然后SenderTask用于2ms发送一个数据,从1-10000循环,ReceiverTask 用于1000ms接收一波数据,大概是500个左右,所以队列的大小我定为510 ,虽然有不小的浪费,但是至少不会数据溢出。最后的Monitor_Task用于判定是否争取的发送和接受,所以这个时候就要用到两个全局变量,在10000ms的周期内判定一次是否两个数据和相等,如果发送的数据之和等于接受的数据之和,那么就毫无疑问的,发送没有问题。而且由于我规定,优先级上,Monitor>Receiver>Sender ,所以可以保证不会发生数据竞争导致的错漏。如果正确,那就亮绿灯,错误就是红灯,事实显示,一直是正确的!!

/**
  ******************************************************************************
  * @file    IO_Toggle/main.c 
  * @author  MCD Application Team
  * @version V1.0.0
  * @date    19-September-2011
  * @brief   Main program body
  ******************************************************************************
  * @attention
  *
  * THE PRESENT FIRMWARE WHICH IS FOR GUIDANCE ONLY AIMS AT PROVIDING CUSTOMERS
  * WITH CODING INFORMATION REGARDING THEIR PRODUCTS IN ORDER FOR THEM TO SAVE
  * TIME. AS A RESULT, STMICROELECTRONICS SHALL NOT BE HELD LIABLE FOR ANY
  * DIRECT, INDIRECT OR CONSEQUENTIAL DAMAGES WITH RESPECT TO ANY CLAIMS ARISING
  * FROM THE CONTENT OF SUCH FIRMWARE AND/OR THE USE MADE BY CUSTOMERS OF THE
  * CODING INFORMATION CONTAINED HEREIN IN CONNECTION WITH THEIR PRODUCTS.
  *
  * <h2><center>© COPYRIGHT 2011 STMicroelectronics</center></h2>
  ******************************************************************************  
  */ 

/* Includes ------------------------------------------------------------------*/
#include "stm32f429i_discovery.h"
#include "stm32f4xx_gpio.h"
#include "stm32f4xx_rcc.h"

/* Kernel includes. */
#include "FreeRTOS.h"
#include "task.h"
#include "timers.h"
#include "queue.h"

/** @addtogroup STM32F4_Discovery_Peripheral_Examples
  * @{
  */

/** @addtogroup IO_Toggle
  * @{
  */ 

/* Private typedef -----------------------------------------------------------*/
GPIO_InitTypeDef  GPIO_InitStructure;

/* Private define ------------------------------------------------------------*/
/* Private macro -------------------------------------------------------------*/
/* Private variables ---------------------------------------------------------*/
/* Private function prototypes -----------------------------------------------*/

/* Private functions ---------------------------------------------------------*/
void Hardware_Init(void);
void Red_LED_On(void);
void Red_LED_Off(void);
void Green_LED_On(void);
void Green_LED_Off(void);
void ToggleLED1_Task(void*);
void ToggleLED2_Task(void*);
/**
  * @brief  Main program
  * @param  None
  * @retval None
  */


//张照博自己写的--START
int32_t Send_Sum=0;
int32_t Received_Sum=0;
   //建立队列 
xQueueHandle MyQueue; 
void Sender_Task(void *pvParameters)
{
    int32_t Send_Num = 1; 
    for( ;; )  
    {  
        vTaskDelay( 2 / portTICK_RATE_MS );
        if (Send_Num>10000)
        {
          Send_Num=1;
        }
        /* 向队列中填充内容 */  
        xQueueSendToBack( MyQueue, ( void* )&Send_Num, 0 );   
        *((int32_t*)pvParameters)+=Send_Sum;
        Send_Num++; 
     }
}

void Receiver_Task(void *pvParameters)
{
  int32_t  Received_Num = 0;  
    for( ;; )  
    {  
        /* 从队列中获取内容 */  
        if( xQueueReceive( MyQueue, &Received_Num, 1000 / portTICK_RATE_MS ) == pdTRUE)  
        {  

          *((int32_t*)pvParameters)+=Received_Num;
           Received_Num=*((int32_t*)pvParameters);
        }  
    }  
}

void Monitor_Task(void *pvParameters)
{
  vTaskDelay( 10000 / portTICK_RATE_MS );  
  if(Send_Sum-Received_Sum<5 && Send_Sum-Received_Sum>-5)
    {
      Green_LED_On();
      Red_LED_Off();
      Send_Sum=0;
      Received_Sum=0;
    } 
  else 
    {
      Green_LED_Off();
      Red_LED_On();
      Send_Sum=0;
      Received_Sum=0;
    }
}

//张照博自己写的--END

int main(void)
{
  /*!< At this stage the microcontroller clock setting is already configured, 
       this is done through SystemInit() function which is called from startup
       file (startup_stm32f4xx.s) before to branch to application main.
       To reconfigure the default setting of SystemInit() function, refer to
        system_stm32f4xx.c file
     */
    Hardware_Init();
    // 初始化硬件平台  
    //prvSetupHardware();  
    //创建全局变量
 MyQueue = xQueueCreate( 510 , sizeof( int32_t ) ); 
    // 建立任务  
    xTaskCreate( Sender_Task, ( signed portCHAR * ) "Sender_Task", configMINIMAL_STACK_SIZE,(void*)&Send_Sum, tskIDLE_PRIORITY+3, NULL );  
    xTaskCreate( Receiver_Task, ( signed portCHAR * ) "Receiver_Task", configMINIMAL_STACK_SIZE,(void*)&Received_Sum, tskIDLE_PRIORITY+4, NULL );   
    xTaskCreate( Monitor_Task, ( signed portCHAR * ) "Monitor_Task", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY+5, NULL ); 
  //启动OS  
    vTaskStartScheduler();  
    return 0; 
 //我的第一次调试,需要去掉后面的
       /* Init and start tracing*/
         vTraceEnable(TRC_INIT);
         vTraceEnable(TRC_START);

 //       /* Create tasks */
 //       xTaskCreate(
    //    ToggleLED1_Task,                 /* Function pointer */
    //    "Task_LED1",                          /* Task name - for debugging only*/
    //    configMINIMAL_STACK_SIZE,         /* Stack depth in words */
    //    (void*) NULL,                     /* Pointer to tasks arguments (parameter) */
    //    tskIDLE_PRIORITY + 3UL,           /* Task priority*/
    //    NULL                              /* Task handle */
 //       );

 //       xTaskCreate(
    //    ToggleLED2_Task,                 /* Function pointer */
    //    "Task_LED2",                           Task name - for debugging only
    //    configMINIMAL_STACK_SIZE,         /* Stack depth in words */
    //    (void*) NULL,                     /* Pointer to tasks arguments (parameter) */
    //    tskIDLE_PRIORITY + 2UL,           /* Task priority*/
    //    NULL                              /* Task handle */
 //       );

    // /* Start the scheduler. */
    // vTaskStartScheduler();

    // /* If all is well, the scheduler will now be running, and the following line
    // will never be reached.  If the following line does execute, then there was
    // insufficient FreeRTOS heap memory available for the idle and/or timer tasks
    // to be created.  See the memory management section on the FreeRTOS web site
    // for more details. */
    // for( ;; );
//我的第一次调试,去掉的位置结尾


}


/**
 * Hardware_Init: 
 */
void Hardware_Init(void)
{
        /* GPIOG Periph clock enable */
        RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG, ENABLE);

        /* Configure PG13, PG14 in output pushpull mode */
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13| GPIO_Pin_14;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
        GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
        GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
        GPIO_Init(GPIOG, &GPIO_InitStructure);

}
/**
 * Red_LED_On: 
 */
void Red_LED_On(void)
{
//    GPIO_SetBits(GPIOG, GPIO_Pin_14);
    GPIOG->ODR |= 0x4000;
}

/**
 * Red_LED_Off: 
 */
void Red_LED_Off(void)
{
//    GPIO_ResetBits(GPIOG, GPIO_Pin_14);
  GPIOG->ODR &= 0xBFFF;
}

/**
 * Green_LED_On: 
 */
void Green_LED_On(void)
{
//    GPIO_SetBits(GPIOG, GPIO_Pin_13);
    GPIOG->ODR |= 0x2000;
}

/**
 * Green_LED_Off: 
 */
void Green_LED_Off(void)
{
//    GPIO_ResetBits(GPIOG, GPIO_Pin_13);
    GPIOG->ODR &= 0xDFFF;
}
/**
 * ToggleLED1_Task: Toggle LED1 via RTOS Timer
 */
void ToggleLED1_Task(void *pvParameters)
{
    int led = 0;  

    while (1) 
    {
        if(led == 0)
        {
            Red_LED_On();
            led = 1;
        } 
        else
        {
            Red_LED_Off();
            led = 0;
         }
        /*
        Delay for a period of time. vTaskDelay() places the task into
        the Blocked state until the period has expired.
        The delay period is spacified in 'ticks'. We can convert
        yhis in milisecond with the constant portTICK_RATE_MS.
        */
        vTaskDelay(1000 / portTICK_RATE_MS);
  }
}

/**
 * ToggleLED2_Task: Toggle LED2 via RTOS Timer
 */
void ToggleLED2_Task(void *pvParameters)

{
    int led = 0;  
    while (1) 
    {
        if(led == 0)
        {
            Green_LED_On();
            led = 1;
        } 
        else
        {
            Green_LED_Off();
            led = 0;
         }
        /*
        Delay for a period of time. vTaskDelay() places the task into
        the Blocked state until the period has expired.
        The delay period is spacified in 'ticks'. We can convert
        yhis in milisecond with the constant portTICK_RATE_MS.
        */
        vTaskDelay(2000 / portTICK_RATE_MS);
  }
}

void vApplicationTickHook( void )
{
}
/*-----------------------------------------------------------*/


/*-----------------------------------------------------------*/

void vApplicationMallocFailedHook( void )
{
    /* vApplicationMallocFailedHook() will only be called if
    configUSE_MALLOC_FAILED_HOOK is set to 1 in FreeRTOSConfig.h.  It is a hook
    function that will get called if a call to pvPortMalloc() fails.
    pvPortMalloc() is called internally by the kernel whenever a task, queue,
    timer or semaphore is created.  It is also called by various parts of the
    demo application.  If heap_1.c or heap_2.c are used, then the size of the
    heap available to pvPortMalloc() is defined by configTOTAL_HEAP_SIZE in
    FreeRTOSConfig.h, and the xPortGetFreeHeapSize() API function can be used
    to query the size of free heap space that remains (although it does not
    provide information on how the remaining heap might be fragmented). */
    taskDISABLE_INTERRUPTS();
    for( ;; );
}
/*-----------------------------------------------------------*/

void vApplicationIdleHook( void )
{
    /* vApplicationIdleHook() will only be called if configUSE_IDLE_HOOK is set
    to 1 in FreeRTOSConfig.h.  It will be called on each iteration of the idle
    task.  It is essential that code added to this hook function never attempts
    to block in any way (for example, call xQueueReceive() with a block time
    specified, or call vTaskDelay()).  If the application makes use of the
    vTaskDelete() API function (as this demo application does) then it is also
    important that vApplicationIdleHook() is permitted to return to its calling
    function, because it is the responsibility of the idle task to clean up
    memory allocated by the kernel to any task that has since been deleted. */
}
/*-----------------------------------------------------------*/

void vApplicationStackOverflowHook( TaskHandle_t pxTask, char *pcTaskName )
{
    ( void ) pcTaskName;
    ( void ) pxTask;

    /* Run time stack overflow checking is performed if
    configCHECK_FOR_STACK_OVERFLOW is defined to 1 or 2.  This hook
    function is called if a stack overflow is detected. */
    taskDISABLE_INTERRUPTS();
    for( ;; );
}
/*-----------------------------------------------------------*/
#ifdef  USE_FULL_ASSERT

/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t* file, uint32_t line)
{ 
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */

  /* Infinite loop */
  while (1)
  {
  }
}
#endif

/**
  * @}
  */ 

/**
  * @}
  */ 

/******************* (C) COPYRIGHT 2011 STMicroelectronics *****END OF FILE****/

原文发布于微信公众号 - 工科狗和生物喵(gh_3507b116a1f8)

原文发表时间:2017-10-29

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java架构师历程

solr

Solr它是一种开放源码的、基于 Lucene Java 的搜索服务器,易于加入到 Web 应用程序中。Solr 提供了层面搜索(就是统计)、命中醒目显示并且支...

4302
来自专栏Java后端技术栈

【面试题】2018年最全Java面试通关秘籍第三套!

注:本文是从众多面试者的面试经验中整理而来,其中不少是本人出的一些题目,网络资源众多,如有雷同,纯属巧合!禁止一切形式的碰瓷行为!未经允许禁止一切形式的转载和复...

1331
来自专栏微信公众号:Java团长

即将发布的 JDK 10 有 109 项新特性,你喜欢哪些?

按计划,JDK 10 将于 3 月 20 日正式发布。据前 Oracle 员工 Simon Ritter 的统计,JDK 10 总共包含 109 项新特性。当然...

702
来自专栏大内老A

WCF后续之旅(11): 关于并发、回调的线程关联性(Thread Affinity)

对于一般的多线程操作,比如异步地进行基于文件系统的IO操作;异步地调用Web Service;或者是异步地进行数据库访问等等,是和具体的线程无关的。也就是说,对...

23810
来自专栏圣杰的专栏

ABP入门系列(10)——扩展AbpSession

源码路径:Github-LearningMpaAbp 一、AbpSession是Session吗? 1、首先来看看它们分别对应的类型是什么? 查看源码发现Ses...

3696
来自专栏Java成神之路

分布式_事务_02_2PC框架raincat源码解析

上一节已经将raincat demo工程运行起来了,这一节来分析下raincat的源码

2271
来自专栏大内老A

WCF后续之旅(11): 关于并发、回调的线程关联性(Thread Affinity)

对于一般的多线程操作,比如异步地进行基于文件系统的IO操作;异步地调用Web Service;或者是异步地进行数据库访问等等,是和具体的线程无关的。也就是说,对...

1957
来自专栏纯洁的微笑

springboot(十一):Spring boot中mongodb的使用

mongodb是最早热门非关系数据库的之一,使用也比较普遍,一般会用做离线数据分析来使用,放到内网的居多。由于很多公司使用了云服务,服务器默认都开放了外网地址,...

3686
来自专栏林冠宏的技术文章

关于Android中为什么主线程不会因为Looper.loop()里的死循环卡死?引发的思考,事实可能不是一个 epoll 那么 简单。

( 转载请务必标明出处:https://cloud.tencent.com/developer/user/1148436/activities) 前序 本文将...

3395
来自专栏Golang语言社区

Go语言开发RESTFul JSON API

也许我们之前有使用过各种各样的API, 当我们遇到设计很糟糕的API的时候,简直感觉崩溃至极。希望通过本文之后,能对设计良好的RESTful API有一个初步认...

1.3K3

扫码关注云+社区

领取腾讯云代金券