资讯动态

GD32H759工控实战:SDRAM、SDIO与触摸屏驱动全攻略

发布时间:2026/9/20 19:21:42 来源:尧图企业网站定制
GD32H759 这颗料在工控圈子里出现频率越来越高Cortex-M7 内核跑到 600MHz 的主频外设资源又给的特别足很多做工业 HMI、数据采集、运动控制、机器视觉预处理的兄弟都开始往这颗芯片上迁。第 4 篇教程我集中把 SDRAM、SDIO 和触摸屏这三个模块过一遍这三个外设组合在一起基本就是一台小型工控主机的核心骨架SDRAM 负责撑起大内存空间SDIO 负责把数据沉淀下来触摸屏负责人和机器之间的交互。这篇面向正在用 GD32H759 搭 RT-Thread 工程、想一口气把显示存储和输入通路打通的人我已经把工程里踩过的坑、验证过的参数、以及 RT-Thread 下的设备挂载方式都整理好了可以直接对照着查。1. 为什么这三个外设是工控主板的“铁三角”1.1 SDRAM、SDIO、触摸屏在工控场景中的角色很多刚接触 GD32H759 的朋友第一反应是这颗片子内部已经有那么大 Flash 和 SRAM 了为什么还要外挂 SDRAM你把 GD32H759 放到真实工控整机里看就明白了它通常不只是跑一个裸机点灯程序而是要跑 RT-Thread 完整的组件生态有时候还带 LVGL 或者柿饼 UI 这类图形界面做实时监控。芯片内置的 SRAM 虽然速度还不错但容量就那么几百 KB跑个带缓存帧的 GUI 就非常吃紧。外挂一片 32MB 或者 64MB 的 SDRAM其实是把 MCU 的应用边界从“能跑”扩展到了“能跑复杂系统”。UI 的显示缓冲、网络协议栈的收发缓冲、多路传感器的原始数据缓存统统可以往里丢性能一下子就上来了。SDIO 则是工控数据持久化最常用的接口。前面系统运行起来以后现场产生的报警记录、工艺参数、曲线数据、系统日志总得有个地方存。挂 TF 卡或者 SD 卡通过文件系统统一管理这是成本低、维护方便、可移植性又强的方案。相比直接在 Flash 上做日志SD 卡的容量大、更换方便而且导数据也简单插上读卡器拷贝就行不用额外写一堆上位机工具。对于需要连续记录数据的数据采集站SDIO 更是不可替代的通道。触摸屏的作用就不用多说了人机交互的直接入口。工业现场可不像实验室操作工戴着劳保手套屏幕表面还可能沾油污和水汽触摸屏的稳定性和准确度直接影响生产效率。GD32H759 本身带了 TFT-LCD 控制器可以直接驱动 RGB 接口的工业屏再配一个可靠的触摸芯片就能在 RT-Thread 上构建一套完整的图形化操作界面。从硬件上看这三者分工明确SDRAM 解决内存容量SDIO 解决数据落盘触摸屏解决人机通路。但这三个外设恰恰也是工程上最容易翻车的地方一个是时序调不好就莫名死机一个是卡识别不稳定、读写卡顿还有一个是触摸坐标漂移、点击误触每一个都得认真对待。1.2 GD32H759 的平台优势GD32H759 之所以适合这类工控应用除了主频高以外更关键的是存储控制器、SDIO 接口和显示接口的硬件设计都比较完整。SDRAM 控制器支持常用的 16 位 SDRAM 颗粒可以灵活配置行列地址宽度刷新周期也能软件调整。SDIO 接口则兼容 SD 2.0 规范最高传输速率做数据记录完全够用。显示接口方面TFT-LCD 控制器支持多种分辨率能直接输出 RGB565 或者 RGB888 格式的信号这样外接屏幕就不用再单独加驱动芯片成本能省不少。RT-Thread 在这颗芯片上的适配也比较成熟。设备驱动框架提供了统一的 SDRAM 内存设备管理方式SDIO 驱动可以配合 DFS 文件系统轻松实现文件操作触摸屏则可以通过 input 设备框架和 GUI 组件无缝衔接。你不需要去研究一大堆底层寄存器的调用逻辑只要把驱动挂对、配置好设备树或者 BSP 里的对应选项就能很快玩起来。下面我按模块逐个讲从初始化到验证从理论参数到实际踩坑尽量把每个细节都交代清楚。如果你是刚开始移植或者调不通建议照着顺序走一遍。2. SDRAM扩展内存让 GUI 和数据缓冲不再捉襟见肘2.1 硬件选型与接线要点SDRAM 的硬件选型主要看容量、位宽和速度等级。工控场景里最常用的是 16 位宽的 SDRAM 颗粒比如 W9825G6KH、IS42S16400J 这类单颗容量从 16Mbit 到 512Mbit 不等。开发板上常见的配置是外接一颗 32MB 的 SDRAM总线位宽 16 位地址线 A0-A12Bank 地址 BA0-BA1数据线 D0-D15再加上时钟、时钟使能、片选、行选通、列选通、写使能等控制信号。接线的关键点是数据线、地址线尽量等长特别是数据线 D0-D15 和 DQM 信号如果 PCB 走线长短差异太大高速时钟下容易出现建立保持时间不够导致读出来的数据偶发错误。硬件上还有一个容易被忽略的点是 SDRAM 的 VDD 去耦电容。SDRAM 在突发读写时电流变化很剧烈如果电源去耦没做好VDD 纹波会直接影响时序稳定性。我之前在一块手焊板上遇到过 SDRAM 读写偶发死机最后用示波器量 SDRAM 供电脚才发现纹波超过了 200mV在 VDD 和 GND 之间补了几个 100nF 和 10uF 的电容后问题就好了。所以画板的时候SDRAM 附近一定要预留足够的去耦电容位置别省这点物料成本。2.2 RT-Thread 下的初始化流程SDRAM 初始化在 RT-Thread 里通常做在系统启动的早期阶段也就是 board_init 或者 BSP 初始化代码里。因为后续的堆内存分配、页面分配器、LVGL 的缓冲池可能都要用到这块外部内存所以它必须在 main 函数之前就准备完毕。GD32H759 的 SDRAM 控制器初始化大致可以分为四步配置控制器时钟和引脚复用配置 SDRAM 时序寄存器发送 SDRAM 初始化命令序列最后做一次数据读写校验。时钟和引脚复用的部分依赖具体的 BSP需要确认 EXMC 外设时钟已经打开同时把 EXMC 相关的 GPIO 复用功能配置到正确的引脚上。不同的开发板引脚分配会不一样这部分直接参考硬件原理图来配不要照搬其他板子的配置。时序寄存器的配置是核心主要包括刷新周期、CAS 延迟、行预充电时间、行激活时间等。典型值可以参考 SDRAM 芯片数据手册里的 AC 特性表。比如常见的 W9825G6KH在 133MHz 时钟下 CAS 延迟设为 3TRCD 和 TRP 设为 3 个时钟周期TRC 大概在 9 到 10 个周期附近刷新周期按 7.8125us 计算周期数。以下是简化版的初始化函数片段核心思想是先把时序寄存器配置到位再执行空操作指令和预充电。void sdram_init(void) { exmc_sdram_timing_parameter_struct timing; exmc_sdram_parameter_struct sdram_init_struct; /* 使能 EXMC 时钟和 GPIO 时钟 */ rcu_periph_clock_enable(RCU_GPIOC); rcu_periph_clock_enable(RCU_GPIOE); rcu_periph_clock_enable(RCU_EXMC); /* 配置 EXMC 引脚复用省略具体引脚枚举 */ timing.load_to_load_delay 2; timing.load_to_active_delay 2; timing.exit_selfrefresh_delay 8; timing.selfrefresh_time 8; timing.row_cycle_delay 10; timing.row_precharge_delay 3; timing.row_active_delay 3; timing.write_recovery_time 2; timing.read_to_write_delay 3; timing.read_latency 3; timing.cas_latency 3; exmc_sdram_init(EXMC_SDRAM_DEVICE_DEV, sdram_init_struct); exmc_sdram_timing_config(EXMC_SDRAM_DEVICE_DEV, timing); /* 执行 SDRAM 初始化序列 */ exmc_sdram_command_config(EXMC_SDRAM_DEVICE_DEV, EXMC_SDRAM_NORMAL_OP, 0, 0); exmc_sdram_command_config(EXMC_SDRAM_DEVICE_DEV, EXMC_SDRAM_PRECHARGE_ALL, 0, 0); exmc_sdram_command_config(EXMC_SDRAM_DEVICE_DEV, EXMC_SDRAM_AUTO_REFRESH, 0, 4); exmc_sdram_command_config(EXMC_SDRAM_DEVICE_DEV, EXMC_SDRAM_MODE_SET, 0, 0x33); }命令配置完成后SDRAM 就进入了正常工作状态这时可以把整个外部内存区域加入 RT-Thread 的内存堆管理。常见做法是使用rt_memheap将外部 SDRAM 挂到系统堆里之后通过rt_malloc申请的内存默认还是走内部 SRAM需要大块缓冲时再显式从 SDRAM heap 里分配。如果你希望 GUI 控件、消息队列这些对象的内存统一使用 SDRAM也可以在链接脚本或者初始化顺序上把主堆调整到 SDRAM 区域但这会让中断上下文的内存分配变慢需要结合项目场景权衡。2.3 时序调参和验证方法时序参数不是随便抄一个就能跑最好通过软件写读校验来验证配置是否正确。我习惯的验证方式是在 SDRAM 初始化完成后对整个内存范围执行一次全地址 32 位数据的写读比较写入模式采用递增地址异或固定值这样可以同时检查地址线短断路和数据位短断路。如果校验误码率很高优先检查地址线和数据线如果只是偶发错误优先检查时序。调试的时候建议先用较低的时钟频率跑通再逐步提高频率。很多芯片默认配置会把 SDRAM 时钟分频系数设成 2 分频也就是如果系统主频 600MHzSDRAM 时钟就是 300MHz但这对于普通商用 SDRAM 颗粒来说太高了。正确的做法是先用 4 分频甚至 6 分频确认读写稳定后再试着提高。还有刷新周期的寄存器配置要特别注意刷新太频繁浪费带宽刷新太少会导致数据丢失典型规则是以 7.8125us 为周期根据实际时钟频率计算出刷新计数值。在这方面我踩过一次坑刷新计数因为漏了个加一操作导致内存区域每隔一段时间就出现零星的数据错误最后定位到是 SDRAM 某一行的数据失效排查了很久才确认是刷新周期没有设置对。对于工控环境SDRAM 时序最好预留一点余量CL 能取 3 就不取 2TRCD 和 TRP 能用 3 个周期就不刻意压缩到 2 个周期。因为工业现场的电磁干扰、温度变化都会让时序余量缩小省那一个时钟周期的性能换来的是整机的稳定性完全值得。3. SDIO工控数据的持久化之道3.1 SDIO 接口和协议基础SDIO 接口工作在 SD 卡和主机之间物理上包括时钟线 CLK、命令线 CMD 和 4 根数据线 D0-D3采用命令-响应机制。初始化阶段主机发送 CMD0 让卡进入空闲状态然后依次发送 CMD8、ACMD41 等命令检测卡的类型和工作电压范围再通过 CMD2、CMD3 获取 CID 和 RCA 地址最后通过 CMD7 选中卡之后就可以发送 CMD17/CMD18 读扇区、CMD24/CMD25 写扇区。RT-Thread 的 SDIO 驱动框架把这一整套协议流程封装好了你基本不用关心这些命令细节只要把底层寄存器读写和中断配置做好上层就能直接认识这个 SD 卡设备。不过理解这个协议过程还是很有必要的因为很多卡识别失败的问题其实就是某个命令环节没有走通。比如有些劣质卡不支持 CMD8导致主机无法正确判断卡类型有些卡对供电电压要求比较高如果 SDIO 电源设计余量不足ACMD41 初始化就会反复超时。为了排查这种问题建议在驱动代码里打开调试输出把命令和响应的错误状态打印出来看看到底卡在哪一步。3.2 在 RT-Thread 上挂上 SD 卡并实现文件系统RT-Thread 下的 SDIO 驱动通常写在 BSP 的 drv_sdio.c 里面做完底层寄存器适配之后SD 卡会被识别为一个块设备然后通过 DFSDevice File System框架和 FALFlash Abstraction Layer配合使用。建议直接使用 RT-Thread 的 SDIO 设备驱动框架初始化函数需要调用rt_hw_sdcard_init()再结合dfs_elm文件系统组件使用 FAT 文件系统格式化 SD 卡。SD 卡上的文件操作会变成标准的 POSIX 接口比如open、read、write、close上层应用写日志、存数据都变得非常简单。文件系统挂载的逻辑可以在 main 函数里执行也可以在 board 初始化阶段执行。我一般在系统启动后单独开一个初始化线程来处理文件系统挂载因为 SD 卡插入检测可能会耗时如果卡插入比较慢阻塞在 main 函数里会导致其他任务启动延迟。挂载的方式大概是先调用dfs_mount将分区挂载到根目录如果没有文件系统再调用dfs_mkfs格式化一次。static void sdcard_mount_thread(void *param) { while (rt_device_find(sd0) RT_NULL) { rt_thread_mdelay(50); } if (dfs_mount(sd0, /, elm, 0, 0) ! RT_EOK) { rt_kprintf(sdcard mount failed, try to format...\n); dfs_mkfs(elm, sd0); if (dfs_mount(sd0, /, elm, 0, 0) ! RT_EOK) { rt_kprintf(sdcard mount failed again\n); return; } } rt_kprintf(sdcard mount success\n); }这个写法虽然简单但在工控场景下很实用。SD 卡换了一张没有文件系统的卡系统能够自动格式化后继续工作不会卡在启动流程上。注意不要在初始化线程里直接做耗时操作要放到独立线程并降低优先级避免阻塞系统调度。3.3 读写性能、磨损均衡与异常断电保护工控系统里经常需要高强度写日志这时候 SD 卡的读写性能就不是一个简单的“能不能用”的问题了而是要评估吞吐量是否满足业务需求。实测下来在 GD32H759 的 SDIO 接口上使用 1 位模式读写顺序读速度能做到 2MB/s 左右写速度在 1.5MB/s 左右。如果使用 4 位模式读写速度都会明显提升读可以到 8MB/s 以上写也能到 5MB/s。需要注意的是如果你需要高吞吐的数据采集一定要验证一次 DMA 传输的数据缓冲区和 SDRAM 之间的配合避免因为内存拷贝导致性能损耗。还有一个必须提醒的问题FAT 文件系统的日志记录频繁的小数据写操作很容易造成 SD 卡寿命下降。工业现场如果三天两头写日志一张普通消费级 SD 卡可能坚持不了几个月。建议的做法是设置一个日志缓冲攒够一定量之后一次性写入减少擦写次数。另外在异常断电保护方面要在主控的掉电检测中断里尽量把文件系统同步和关闭做好例如调用sync或者dfs_msync虽然彻底断电后可能会丢最后几条日志但至少能避免文件系统整体损坏。如果你要做真正的工业级存储更稳妥的方案是选用工业级 SD 卡或者 eMMC同时在文件系统层使用掉电保护设计。RT-Thread 的 DFS 框架也支持 LittleFS 这类具备掉电安全特性的文件系统在需要高可靠性的场景下可以考虑。4. 触摸屏人机交互的最后一公里4.1 触摸芯片的类型与接口方式工业触摸屏按照原理分主要就是电阻式、电容式和红外式。电阻式触摸屏成本低、能带手套操作但透光率差一些材料老化之后容易失准电容式触摸屏手感好、支持多点触控但成本高对现场手上有水或者戴厚手套的情况不太友好红外触摸屏适合大尺寸屏幕抗干扰能力强但体积大、功耗高。GD32H759 项目中常见的选择是搭配电容式触摸芯片通过 I2C 接口和 MCU 通信触摸芯片内部已经完成了坐标计算和滤波处理主控只需要读寄存器和处理中断。常用的电容触摸芯片如 GT9147、GT911、FT5426这些芯片大多使用 I2C 接口支持最高 5 点触控坐标输出精度在 10 bit 左右。芯片一般还有一个中断引脚 INT 和一个复位引脚 RESET工作模式下触摸事件会拉低中断引脚主控在中断触发后再通过 I2C 读取坐标数据。电阻式触摸屏的话常用芯片有 XPT2046 和 NS2009通过 SPI 接口输出原始 ADC 值主控需要自己做四点校准和滤波处理起来会复杂一些但胜在结构简单、成本极低。4.2 RT-Thread touch 框架与设备适配RT-Thread 对触摸设备提供了统一的输入设备框架设备类型是RT_DEVICE_CLASS_TOUCH驱动层需要实现读取坐标、控制采样等操作然后注册到系统中。注册完成后上层 GUI 组件就可以通过rt_device_read来获取触摸数据也可以使用消息队列把触摸事件推送给 GUI 线程。具体适配的时候最关键的是配置触摸芯片的 I2C 工作频率。大多数电容触摸芯片 I2C 最大支持 400kHz如果主控侧把 I2C 时钟配置成 1MHz 以上数据读取就会偶发错位导致坐标跳变。触摸芯片的中断处理推荐使用中断线程加信号量的方式。以下是一个典型的触摸中断回调设计中断里只做事件标记真正读取坐标数据放到一个高优先级线程里执行避免在中断上下文中长时间阻塞 I2C 总线。static struct rt_semaphore touch_sem; static void touch_int_callback(void *args) { rt_sem_release(touch_sem); } static void touch_read_thread(void *param) { struct rt_touch_data *tdata; while (1) { rt_sem_take(touch_sem, RT_WAITING_FOREVER); rt_device_read(touch_dev, 0, tdata, 1); /* 把坐标和按下状态投递到 GUI 事件队列 */ } }触摸芯片的复位时序也值得注意。很多芯片要求上电后先拉低 RESET 再拉高并保持至少 10ms 以上然后再等待几十毫秒才进入正常工作状态。如果复位时序不对I2C 枚举时可能找不到设备地址表现为触摸完全没反应。我一般会在触摸驱动初始化时直接操作 GPIO 控制复位引脚并且在读取设备 ID 成功之后才注册设备这样可以尽早发现硬件问题。4.3 校准、滤波与防误触电阻式触摸屏必须做软件校准因为屏幕和触摸膜之间存在机械误差和旋转偏移。常用的校准算法是三点校准或者五点校准通过读取屏幕四个角和中心点的 ADC 原始值建立线性变换矩阵。具体做法是在屏幕上依次显示几个校准点记录每个点的原始值和目标坐标然后用最小二乘法或者直接解方程算出变换系数。校准数据可以保存在非易失存储器里下次开机直接加载避免每次都要人工校准。电容式触摸屏一般出厂自带了坐标线性化处理正常情况下不需要做复杂的校准但仍然要做防误触处理。工控现场经常有油污、水滴落在屏幕上如果芯片不做防水滤波就可能在屏幕上出现“鬼点”导致 GUI 界面误触发按键。处理方式是在触摸驱动或者应用层增加一个简单的滤波逻辑连续 N 次读取到坐标变化幅度小于某个阈值才认为是一个有效的稳定触摸如果一段时间内检测到 5 个以上的触点同时按下则判定为误触并忽略事件。这种方案对防止金属屑、水渍造成的误触很有效。在实际项目中我还会把触摸坐标的扫描和显示刷新放到同一个线程里保证触摸事件到界面反馈的延迟尽量低。一个简单的调试方法是用示波器测触摸中断引脚的电平变化如果中断频率很高说明环境干扰严重需要通过滤波和软件消抖来抑制。面板上如果使用了偏光膜或是比较厚的玻璃触摸灵敏度也会降低这时需要调整触摸芯片的接收增益参数让触摸的阈值更合适。5. 三个模块联调实录与避坑心得5.1 启动阶段最常见的死机问题联调的时候最容易遇到的问题就是启动阶段死机而且很多死机的原因并不是 CPU 本身跑飞了而是 SDRAM 初始化后还没有进入稳定状态RT-Thread 内核就在外部内存上建立了堆和信号量等数据结构导致一跑就崩。表现是程序下载完第一次上电基本正常第二次复位就卡死打印信息停在内核启动的某个位置。这个问题十有八九是 SDRAM 的初始化时序和时钟频率不匹配。调试的时候我建议打开 RT-Thread 的RT_DEBUG_INIT开关一步步确认 SDRAM 初始化函数返回值再检查全局内存分配中的首块地址。如果确认是外部内存访问异常可以把 SDRAM 时钟频率降一档重新做全地址读写校验。另一种情况是 SDRAM 初始化放在 main 函数里执行但 RT-Thread 内核在此之前已经使用了外部内存区域。解决方案是确保 SDRAM 初始化在rt_hw_board_init阶段完成并且把主堆地址明确设置为内部 SRAM等 SDRAM 校验通过后再在用户线程里使用外部内存。触摸屏和 SDIO 在启动阶段偶尔也会插一脚。触摸芯片初始化时如果 I2C 总线挂死整个系统的初始化线程会被卡住SD 卡初始化时如果短卡或者插了一张损坏的卡也会让文件系统挂载线程反复失败。我的建议是给触摸和 SD 卡初始化都设置超时和重试机制不要因为单个外设异常而拖垮整个系统的启动。工控设备的可靠性不光是硬件冗余软件层面也要有容错设计。5.2 性能优化和降低 CPU 占用系统跑起来之后CPU 占用高的点主要集中在 SDRAM 的数据拷贝、SDIO 的读写传输和触摸数据的轮询处理上。SDRAM 的读写最好通过 DMA 进行比如把数据从外设 DMA 到 SDRAM或者从 SDRAM DMA 到外设避免 CPU 做大量的 memcpy。RT-Thread 的 DMA 框架可以配合内存池使用注意缓冲区地址对齐到 32 字节这样 DMA 的效率才是最高的。SDIO 的读写不要在主线程里同步执行。最好创建一个专门的文件操作线程把日志写入请求放到消息队列里由该线程统一从队列取数据并写入 SD 卡。这样即便 SD 卡写入速度波动也不会阻塞核心控制任务的运行。同时在写文件时尽量采用页对齐的写入长度减少 FAT 文件系统产生碎片的概率既能提升写入速度也能减少卡上文件的碎片化。触摸数据处理的 CPU 占用一般不大但如果你的触摸驱动是高频率轮询也会造成不必要的开销。建议在中断模式下工作如果没有空闲中断引脚也要把轮询周期控制在 20ms 左右这样既能保证触摸响应流畅又不会过多占用 CPU。另外触摸数据从驱动到 GUI 应用之间的传递建议使用发布订阅模式或者直接引用共享内存加信号量避免频繁的动态内存分配。5.3 调试工具和工程实践建议做这类系统级联调示波器和逻辑分析仪是必备的。调试 SDRAM 的时候重点观察时钟线、片选、RAS、CAS 信号是否符合预期如果条件允许用示波器测量读写时的数据线高电平是否稳定。调试 SDIO 时逻辑分析仪可以解析出 SD 卡的初始化命令序列和响应能一眼看出卡在哪个命令环节失败。调试触摸屏时逻辑分析仪可以抓取 I2C 的数据帧确认初始化地址和读取坐标的寄存器是否正常。软件层面建议在 RT-Thread 里开启 FinSH 控制台调试阶段可以直接通过命令行调用命令测试 SDRAM 读写、挂载文件系统、读取触摸坐标。比如注册一条sdrw_test命令输入起始地址和长度就能执行内存读写注册一条touch_test命令按下触摸屏后会打印原始坐标和校准坐标。这种方式比反复下载代码要高效得多。在联调时我还会在触摸数据读取代码里增加一个断言打印一旦读到的坐标超出屏幕分辨率就直接报警这在早期定位触摸参数配置错误的时候非常有用。工程实践方面建议保留一个稳定可回退的基线版本。每次调整 SDRAM 时序或者 SDIO 驱动参数之前先确认当前版本是可编译、可烧录、可运行的不然改挂了以后连一个能对比的参照都没有。还有项目里所有外设驱动的初始化日志建议打印关键寄存器的配置值比如 SDRAM 时钟分频、SDIO 的时钟频率、I2C 的速率这些都打印出来后面查现场问题的时候会成为最关键的线索。我在实际项目里踩过很多次坑最后总结下来的经验就一句话不要相信“配置抄过去就能跑”每个板子的 PCB 布线、电源纹波、外部晶振特性都不一样SDRAM 和 SDIO 这类高速接口必须实测验证。触摸屏也是一样不同批次的屏幕触摸芯片的固件版本可能有差异上报的坐标精度和滤波效果都不一样。宁可前期在验证上多花一点时间也别等产品放到现场天天被客户催着改 bug。这套 GD32H759 方案把 SDRAM、SDIO 和触摸屏这三样打通之后再往上面加 EtherCAT、CANopen 这些工业总线协议或者接上 USB 摄像头做视觉检测整个平台的想象空间非常大。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价