嵌入式系统导论(第2版)-教学课件 嵌入式导论05_第1页
嵌入式系统导论(第2版)-教学课件 嵌入式导论05_第2页
嵌入式系统导论(第2版)-教学课件 嵌入式导论05_第3页
嵌入式系统导论(第2版)-教学课件 嵌入式导论05_第4页
嵌入式系统导论(第2版)-教学课件 嵌入式导论05_第5页
已阅读5页,还剩69页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

嵌入式系统导论主讲教师:第5章STM32的GPIO本章内容提要5.1GPIO的结构和功能5.2GPIO寄存器5.3GPIO输出应用:LED灯的亮灭控制5.4GPIO输入应用:查询按键状态5.5STM32库编程总结主

机外设接口电路参考资料:STM32F10x系列参考手册参考资料:STM32库帮助文档stm32f10x_stdperiph_lib_um.chmSTM32F10xStandardPeripheralsFirmwareLibrary5.1GPIO的结构和功能STM32提供通用的输入输出引脚(GPIO)输出高低电平控制外设输入0、1检测外设状态可配置为复用的输入输出引脚(AFIO)大多数GPIO都有默认的复用功能:可作为片上外设(如串口、ADC等)的I/O引脚部分GPIO还有重映射功能:把原来是A引脚的默认复用功能,映射到B引脚使用GPIO:General-PurposeInputs/OutputsAFIO:Alternate-FunctionInputs/Outputs通用I/O端口GPIOSTM32F10x微控制器有7组(个)GPIO端口用GPIOx(x是A、B、C、D、E、F、G)表示 即GPIOA、GPIOB、……GPIOG每组端口有16个引脚分别用Px0、Px1、……Px15(x是A~G)表示每组端口具有7个寄存器1.配置寄存器:GPIOx_CRL、GPIOx_CRH2.数据寄存器:GPIOx_IDR、GPIOx_ODR3.位控寄存器:GPIOx_BSRR、GPIOx_BRR4.锁定寄存器:GPIOx_LCKRGPIO端口位的基本结构STM32F10x系列参考手册(Referencemanual)5.2GPIO寄存器每组端口具有7个寄存器实现对GPIO端口初始化配置和数据输入输出控制每个寄存器只能以32位(字)进行访问不允许16位(半字)或8位(字节)访问7个寄存器分成4种1.配置寄存器:选择引脚功能,例如输入或输出2.数据寄存器:保存引脚输入电平或输出电平3.位控寄存器:控制某引脚为1或04.锁定寄存器:锁定引脚配置(不允许修改)GPIO:General-PurposeInputs/OutputsGPIO寄存器名称寄存器缩写寄存器英文名称GPIOx_CRLConfigurationRegisterGPIOx_CRHConfigurationRegisterHighGPIOx_IDRInputDataRegisterGPIOx_ODROutputDataRegisterGPIOx_BSRRBitSet/ResetRegisterGPIOx_BRRBitResetRegisterGPIOx_LCKRConfigurationLockRegister1.GPIO的配置寄存器每组端口有两个32位配置寄存器配置寄存器低(Low)字GPIOx_CRL配置寄存器高(High)字GPIOx_CRH(x是A~G)配置寄存器低字CRL对应配置低8位引脚Px0、Px1、……Px7配置寄存器高字CRH对应配置高8位引脚Px8、Px9、……Px15ConfigurationRegisterGPIO的引脚配置两个32位(一个64位)配置寄存器每4位对应一个引脚低2位设置其工作模式(MODE)高2位设置其配置(CNF:Configuration)共有4种输入功能、4种输出功能GPIO的引脚配置功能表CNF[1:0]MODE[1:0]引脚配置的功能0000(输入)模拟输入模式01浮空输入模式(默认)10上拉输入模式10下拉输入模式0001(输出,10MHz)10(输出,2MHz)11(输出,50MHz)通用推挽输出模式01通用开漏输出模式10复用推挽输出模式11复用开漏输出模式输出模式有3种I/O驱动电路的响应速度可选2.GPIO的数据寄存器每组端口有两个32位数据寄存器一个是输入数据寄存器IDR一个是输出数据寄存器ODR都只使用其低16位、依次对应每个GPIO引脚当设置GPIO引脚为输入模式时从输入数据寄存器GPIOx_IDR的相应位读出该I/O引脚的高(1)、低(0)电平当配置GPIO引脚为输出模式时向输出数据寄存器GPIOx_ODR相应位写入1或0控制该I/O引脚为高(1)、低(0)电平DataRegister端口输入数据寄存器(GPIOx_IDR)IDR:InputDataRegister端口输出数据寄存器(GPIOx_ODR)ODR:OutputDataRegister位输出问题对输出数据寄存器某位写入时要考虑其他位的状态,不能任意改变需要首先读出输出数据寄存器的内容修改相应位再写入GPIO的输出数据寄存器ODR可以读出用以支持位输出控制可以使用CM3的位段别名地址访问数据位更方便的位数据输出是使用位控寄存器3.GPIO的位控寄存器每个端口有两个位控寄存器只能写入、不能读出某位写入1实现I/O引脚复位或置位写入0对I/O引脚无影响(作用)一个是位置位/复位寄存器BSRR高16位控制引脚为低电平(复位BR:BitReset)低16位控制引脚为高电平(置位BS:BitSet)一个是位复位寄存器BRR低16位控制引脚为低电平(复位BR:BitReset)BSRR:BitSet/ResetRegister,BRR:BitResetRegister端口位设置/复位寄存器(GPIOx_BSRR)BSRR:BitSet/ResetRegister端口位复位寄存器(GPIOx_BRR)BRR:BitResetRegister4.GPIO的锁定寄存器这个32位的端口配置锁定寄存器GPIOx_LCKR用于冻结配置寄存器对I/O引脚功能的改变当对端口执行了写入锁定序列后被锁定引脚配置的工作模式不能再改变直到下次复位后才被解锁锁定功能可以防止程序随意改变GPIO配置LockRegisterstm32f10x_gpio.h定义的GPIO结构类型每组端口具有7个寄存器依次作为结构成员定义结构体变量(指针)通过结构成员访问寄存器/*定义*/typedefstruct{__IOuint32_tCRL;__IOuint32_tCRH;__IOuint32_tIDR;__IOuint32_tODR;__IOuint32_tBSRR;__IOuint32_tBRR;__IOuint32_tLCKR;}GPIO_TypeDef;#define__IOvolatile/*使用*/

GPIO_TypeDef

GPIOx;

GPIOx.CRL=0;

GPIO_TypeDef*GPIOx;GPIOx->CRL=0;stm32f10x.h定义的GPIO基地址定义基地址后,每组端口 被定义为指向这个基地址的结构类型指针#definePERIPH_BASE((uint32_t)0x40000000)...#defineAPB2PERIPH_BASE(PERIPH_BASE+0x10000)...#defineGPIOA_BASE(APB2PERIPH_BASE+0x0800)#defineGPIOA((GPIO_TypeDef*)GPIOA_BASE)5.3GPIO输出应用示例:LED等的亮灭控制【例5-1】流水灯或跑马灯程序LED在目标板上的连接使用PB0、PF7和PF8依次连接LED1~LED3GPIO引脚输出 低电平、LED亮GPIO引脚输出 高电平、LED灭【例5-1】用户需要创建或编辑的文件主程序,实现主流程代码:

main.c(文件名可以修改)LED驱动程序:

led.c和led.h(文件名可以修改)【例5-1】应用程序主要流程本例中,程序主流程有3个步骤:(1)开启外设时钟

本例是GPIOF(2)初始化外设

本例是连接LED1~LED4的GPIO引脚(3)控制外设工作

本例是置位、复位GPIO引脚1.开启外设时钟使用复位和时钟控制RCC驱动程序 (stm32f10x_rcc.c)有3个针对不同总线连接的外设时钟命令函数RCC_AHBPeriphClockCmdRCC_APB1PeriphClockCmdRCC_APB2PeriphClockCmdGPIO通过APB2总线连接系统开启GPIO外设时钟的函数

RCC_APB2PeriphClockCmd帮助文档RCC_APB2PeriphClockCmd函数GPIO通过APB2总线连接系统开启GPIOB和GPIOF外设时钟

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOF,ENABLE);2.初始化外设1.

定义外设初始化结构变量(PPP是外设名称)

PPP_InitTypeDef

PPP_InitStructure;2.

用允许的成员值填充外设初始化结构成员

PPP_InitStructure.member1=val1; …3.

调用PPP_Init函数初始化外设

PPP_Init(PPP,&PPP_InitStructure);4.

允许外设开始工作(不是所有的外设都需要)

PPP_Cmd(PPP,ENABLE);STM32库给出的外设初始化过程GPIO的初始化函数GPIO_Init

voidGPIO_Init(GPIO_TypeDef*GPIOx,

GPIO_InitTypeDef*GPIO_InitStruct)InitializestheGPIOxperipheralaccordingtothespecifiedparametersintheGPIO_InitStruct.

Parameters:

GPIOx,:wherexcanbe(A..G)toselecttheGPIOperipheral.

GPIO_InitStruct,:pointertoaGPIO_InitTypeDefstructurethatcontainstheconfigurationinformationforthespecifiedGPIOperipheral.Returnvalues: NoneDefinitionatline173offilestm32f10x_gpio.c.帮助文档GPIO的初始化结构类型GPIO_InitTypeDef

typedef

struct{uint16_tGPIO_Pin;

/*指定配置的GPIO引脚*/

GPIOSpeed_TypeDef

GPIO_Speed; /*指定GPIO引脚输出的最高频率*/

GPIOMode_TypeDef

GPIO_Mode;

/*指定GPIO引脚配置的工作模式*/}GPIO_InitTypeDef;定义在GPIO头文件(stm32f10x_gpio.h)GPIO_InitTypeDef成员1:GPIO_Pin要进行配置的GPIO引脚编号其值是常量GPIO_Pin_y(y是0…15和ALL)#defineGPIO_Pin_0 ((uint16_t)0x0001)

/*!<Pin0selected*/#defineGPIO_Pin_1 ((uint16_t)0x0002)

/*!<Pin1selected*/ ……#defineGPIO_Pin_15 ((uint16_t)0x8000)

/*!<Pin15selected*/#defineGPIO_Pin_All ((uint16_t)0xFFFF)

/*!<Allpinsselected*/GPIO_InitTypeDef成员2:GPIO_Speed选择最高输出频率定义在枚举类型GPIOSpeed_TypeDef中typedefenum{GPIO_Speed_10MHz=1,

GPIO_Speed_2MHz,

//不赋值的枚举变量自动加1,故此常量值为2GPIO_Speed_50MHz //常量值为3}GPIOSpeed_TypeDef;GPIO_InitTypeDef成员3:GPIO_Mode选择工作模式定义在枚举类型GPIOMode_TypeDef

中typedef

enum{GPIO_Mode_AIN=0x0, //模拟输入模式

GPIO_Mode_IN_FLOATING=0x04, //浮空输入模式

GPIO_Mode_IPD=0x28, //下拉输入模式

GPIO_Mode_IPU=0x48, //上拉输入模式

GPIO_Mode_Out_OD=0x14, //通用开漏输出模式

GPIO_Mode_Out_PP=0x10, //通用推挽输出模式

GPIO_Mode_AF_OD=0x1C, //复用开漏输出模式

GPIO_Mode_AF_PP=0x18 //复用推挽输出模式}GPIOMode_TypeDef;GPIOB和GPIOF初始化GPIO_InitTypeDef

GPIO_InitStructure;/*配置PB0(LED1)*/GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;GPIO_Init(GPIOB,&GPIO_InitStructure);/*配置PF7(LED2)和PF8(LED3)*/GPIO_InitStructure.GPIO_Pin=GPIO_Pin_7|GPIO_Pin_8;GPIO_Init(GPIOF,&GPIO_InitStructure);3.控制外设工作外设驱动库提供控制外设工作的有关函数对GPIO主要是输入和输出数据本例中只需要输出函数,例如字输出GPIO_Write函数位输出GPIO_WriteBit函数置位GPIO_SetBits函数复位GPIO_ResetBits函数帮助文档

void

GPIO_SetBits(GPIO_TypeDef*GPIOx, uint16_tGPIO_Pin)

voidGPIO_ResetBits(GPIO_TypeDef*GPIOx, uint16_tGPIO_Pin)LED驱动头文件led.h#ifndef__LED_H#define __LED_H#include"stm32f10x.h"

void

LED_Config(void);voidLED_On_all(void);voidLED_Off_all(void);voidLED_On(uint8_tled);voidLED_Off(uint8_tled);voidDelay(__IOuint32_tnCount);#endif /*__LED_H*/LED驱动源程序文件led.c-1#include"led.h"voidLED_Config(void) //LED配置函数

{ //声明外设初始化结构体变量

GPIO_InitTypeDef

GPIO_InitStructure;

//启动外设时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOF,ENABLE);//配置LED1、LED2和LED3

…… //初始化外设:LED灯全灭

GPIO_SetBits(GPIOB,GPIO_Pin_0);GPIO_SetBits(GPIOF,GPIO_Pin_7|GPIO_Pin_8);}LED驱动源程序文件led.c-2

//LED全亮函数

voidLED_On_all(void){

GPIO_ResetBits(GPIOB,GPIO_Pin_0);GPIO_ResetBits(GPIOF,GPIO_Pin_7|GPIO_Pin_8);}//LED全灭函数voidLED_Off_all(void){

GPIO_SetBits(GPIOB,GPIO_Pin_0);GPIO_SetBits(GPIOF,GPIO_Pin_7|GPIO_Pin_8);}LED驱动源程序文件led.c-3

//指定某个LED灯亮函数,不涉及未指定的LED灯

voidLED_On(uint8_tled){switch(led){case1:GPIO_ResetBits(GPIOB,

GPIO_Pin_0);break;

case2:GPIO_ResetBits(GPIOF,

GPIO_Pin_7);break;

case3:GPIO_ResetBits(GPIOF,

GPIO_Pin_8);break;default:break;} }LED驱动源程序文件led.c-4

//指定某个LED灯亮函数,不涉及未指定的LED灯

voidLED_On(uint8_tled){switch(led)

{case1:GPIO_SetBits(GPIOB,GPIO_Pin_0);break;case2:GPIO_SetBits(GPIOF,GPIO_Pin_7);break;case3:GPIO_SetBits(GPIOF,GPIO_Pin_8);break;default:break;}}/*简单延时函数*/voidDelay(__IOuint32_tnCount){for(;nCount!=0;nCount--);}主程序main.c-1

#include"stm32f10x.h"

#include"led.h"

int

main(void)//主函数

{

LED_Config(); //LED初始化

LED_On_all(); //全亮

Delay(5000000);

LED_Off_all();

//全灭

Delay(5000000);主程序main.c-2

while(1){//逐个点亮LED,像跑马灯一样循环

LED_Off_all();

LED_On(1);Delay(1000000); ……

LED_Off_all();

LED_On(3);

Delay(1000000);}}

for(inti=1;i<4;i++)

{

LED_Off_all();

LED_On(i);Delay(1000000);

}程序软件模拟(Simulator)构建工程项目(Project→BuildTarget)启动调试(Debug→Start/StopDebugSession)通过外设(Peripherals)菜单选择GPIO的B端口打开GPIOB寄存器窗口查看其数值以及运行过程中的改变通过查看(View)菜单选择选择分析窗口(AnalysisWindows)的逻辑分析仪(LogicAnalyzer)单击“Setup”按钮设置要观察的外设引脚(本例)PORTB.0、PORTF.7和PORTF.8单步执行观察波形(注意用Zoom放大缩小时间轴)GPIO外设逻辑分析仪的设置逻辑分析仪(LogicAnalyzer)进入while语句:使用单步进入(Step,F11)在while循环中:使用单步通过(StepOver,F10)程序硬件仿真(In-CircuitDebugger)Cortex-M处理器有两种接口调试(Debug)接口:下载程序、调试执行跟踪(Trace)接口:采集数据、事件、性能等常使用JTAG/SWD接口标准连接器硬件仿真的MDK设置在MDK的目标选项Debug和Utilities标签选择使用目标板配套的硬件仿真工具添加CPU支持的Flash设置下载程序后是否自动运行TargetOptions→Debug→Setting→FlashDownload如果不是自动运行,需要目标板手动复位(上电复位或按键复位)选择创建HEX文件(CreateHEXfile)TargetOptions→Output→CreateHEXfile使用在线仿真器安装硬件在线仿真器的驱动程序在目标选项的Debug或Utilities标签中单击“Settings”,弹出仿真器的目标驱动设置(TargetDriverSetup)对话框在“FlashDownload”标签中通过“Add”按钮增加CPU使用的编程算法当打开“Debug”标签时,软件会自动扫描仿真器序列号(SN)选择端口(Port)使用SW才可以在线跟踪程序程序写入目标板命令:Flash→Download5.4GPIO输入应用示例:查询按键状态【例5-2】查询按键状态,控制LED灯亮灭查询方式检测按键KEY状态KEY在目标板上的连接PA0和PC13分别连接KEY1和KEY2按钮按下时, 相应GPIO引脚输入低电平(0), 否则输入高电平(1)【例5-2】查询按键状态引脚可配置为浮空输入模式(GPIO_Mode_IN_FLOATING)也可以配置为上拉输入模式(GPIO_Mode_IN_IPU)程序的功能按下按钮KEYx(x=1,2)对应LEDx亮一段时间, 然后熄灭按键初始函数化KEY_Config

GPIO_InitTypeDef

GPIO_InitStructure; //初始化KEY1(连接PA0)

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);

GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;

GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;

GPIO_Init(GPIOA,&GPIO_InitStructure); //初始化KEY2(连接PC13)

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);

GPIO_InitStructure.GPIO_Pin=GPIO_Pin_13;GPIO_Init(GPIOC,&GPIO_InitStructure);

读入GPIO引脚值帮助文档GPIO驱动程序有两个引脚输入函数GPIO_ReadInputData,读取整个端口的16位GPIO_ReadInputDataBit,读取某个位按键检测使用位读取函数更方便

uint8_tGPIO_ReadInputDataBit

(GPIO_TypeDef*GPIOx,uint16_tGPIO_Pin)GPIOx指定端口:GPIOA…GPIOGGPIO_Pin指定引脚:GPIO_Pin_0…GPIO_Pin_15返回值是输入引脚的数值:0或1按键扫描函数KEY_Scanuint8_tKEY_Scan(uint8_tkey){

switch(key) //检测key按键,按下返回0{case1:return(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0));case2:return(GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_13));

default:return1;

}}主程序main.c的循环检测流程

while(1)

{//逐个检测按键,按下某个键对应LED亮

if(KEY_Scan(1)=

=

0) //KEY1按下

{while(KEY_Scan(1)==0); //等待按键结束

LED_On(1); //点亮LED1Delay(5000000);LED_Off(1);}

if(KEY_Scan(2)=

=0) ……

}GPIO_KEY示例的软件模拟启动调试(Debug→Start/StopDebugSession)打开逻辑分析仪窗口运行程序(Run,F5)软件模拟状态,输入引脚默认为低(0),所以: 按键被按下、但没有释放,导致LED引脚为高电平打开GPIOA或GPIOC窗口,改变PA0或PC13状态:

LED引脚(PB0或PF7)对应改变电平按键模拟编辑调试函数,生成仿真文件(key.ini)在目标选项的调试标签窗口输入初始化文件(InitializationFile)软件模拟的按键钮μVision的调试函数(DebugFunctions)μVision的调试函数用于辅助调试应用程序扩展μVision4调试器的功能产生外部中断将存储器内容记录在文件中周期性地更新模拟输入值给芯片上的串行端口输入串行数据μVision的调试函数使用C语言的一个子集与标准C一样,可使用流程控制语句:ifelsewhiledoswitchcasebreakcontinuegoto

调试函数中可定义局部变量,但不允许使用数组详见μVision4用户指南(μVision4User’sGuide)创建调试函数使用调试函数编辑器将调试函数保存于文件Debug—FunctionEditor开启调试函数编辑器函数编写完,使用其中“compile”命令编译通过编辑完成,保存于文件(扩展名.ini)MDK5的帮助文件:\Keil_v5\ARM\Hlp\uv4.chm软件模拟按键的调试函数(key.ini)DEFINEBUTTON"Key1","Key1Press()"//设置Key1按钮DEFINEBUTTON"Key2","Key2Press()"//设置Key2按钮signalvoidKey1Press(void){//Key1按钮的函数

PORTA|=0x01;//PA0引脚高电平

swatch(0.05);//延时0.05sPORTA&=~0x01;//PA0引脚低电平

swatch(0.05);}//PA0引脚,产生高脉冲模拟一次按键signalvoidKey2Press(void){//Key2按钮的函数

PORTC|=(0x01<<13);swatch(0.05);PORTC&=~(0x01<<13);swatch(0.05);}//PC13引脚,产生高脉冲模拟一次按键使用调试函数可在命令窗口输入函数名及参数运行例如运行printf内置函数:>printf("HelloWorld\n")

将在输出窗口的命令页显示“HelloWorld”将调试函数编辑成为一个仿真文件,然后在命令窗口使用INCLUDE命令执行文件内容>INCLUDEKEY.INI或者在设置目标选项,每次启动调试器自动执行OptionsforTarget-Debug-InitializationFile5.5STM32库编程总结安装MDK-ARM和STM32驱动程序库项目创建、配置目标选项编写外设驱动函数和主程序文件软件模拟、硬件仿真总结1:常量定义各种参数值等定义有常量、或者枚举常量一般常量采用大写英文字母表达枚举常量使用下划线分隔各个字段、单词首字大写枚举类型枚举常量及含义FunctionalStateDISABLE=0:禁止ENABLE=1:允许FlagStatus(ITStatus)RESET=0:复位(清除、清零、置0)SET=1:置位(置1)ErrorStatusERROR=0:错误SUCCESS=1:成功GPIO模式常量:GPIOMode_TypeDef引脚配置的功能STM32枚举常量模拟输入模式GPIO_Mode_AIN浮空输入模式(默认)GPIO_Mode_IN_FLOATING上拉输入模式GPIO_Mode_IPD下拉输入模式GPIO_Mode_IPU通用推挽输出模式GPIO_Mode_Out_OD通用开漏输出模式GPIO_Mode_Out_PP复用推挽输出模式GPIO_Mode_AF_OD复用开漏输出模式GPIO_Mode_AF_PP总结2:外设函数外设函数名用一个下划线分隔两个部分下划线前面部分是该外设的缩写PPP(大写字母)下划线后面部分是反映函数功能的词汇,每个单词首字母采用大写字母、后跟小写字母函数名函数功能PPP_InitPPP初始化PPP_DeInit复位PPP寄存器为默认值PPP_StructInit使用默认数据

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论