在RTT下为什么推荐使用RTT统一标准的SPI接口
在《【龙芯1c库】封装硬件SPI接口和使用示例》http://blog.csdn.net/caogos/article/details/78353988中提到了,龙芯1c的每路SPI有4个片选,在多任务的实时系统中需要注意互斥的问题。龙芯1C库中的SPI接口仅仅是把龙芯1C的SPI硬件相关特别封装成了一个一个的软件接口。而RT-Thread中统一的标准SPI接口刚好解决了互斥的问题。即只要把龙芯1c库中的SPI接口稍加修改,与RTT中统一的标准SPI接口适配好后,就可以实现使用RTT标准SPI接口编程,而不需考虑互斥的问题。下面以函数 rt_spi_transfer()为例来看看RTT是如何实现互斥的。
程序的大概意思是,在SPI收发之前首先获取锁,以此实现互斥。然后判断上一次SPI通信的从设备是否与当前从设备为同一个,对应代码为if (device->bus->owner != device),如果不是,需要调用evice->bus->ops->configure()函数重新配置SPI的时钟频率,极性和相位等,最后才是调用device->bus->ops->xfer()实现具体的SPI收发。
其实,在刚开始接触RTT和龙芯1C时,不用关注这么多细节,我已经把RTT统一的标准的SPI接口移植到龙芯1C上了,只需要知道怎么使用这些接口即可。如果之前在STM32上用过RTT的SPI,那么我告诉你,在龙芯1C上也是一样的。因为是RTT统一的SPI接口和具体的芯片“无关”。下面来看看RTT提供了几个统一的SPI接口,都有什么功能。
RTT统一的标准的SPI接口简介
注册SPI总线
函数原型
/* * 初始化并注册龙芯1c的spi总线 * @SPI SPI总线,比如LS1C_SPI_0, LS1C_SPI_1 * @spi_bus_name 总线名字 * @ret */ rt_err_t ls1c_spi_bus_register(rt_uint8_t SPI, const char *spi_bus_name)
函数ls1c_spi_bus_register()调用RTT的统一标准接口rt_spi_bus_register(),实现SPI总线的注册
使用示例
// SPI模块编号 #define LS1C_SPI_0 (0) #define LS1C_SPI_1 (1) #define LS1C_SPI0_BUS_NAME ("spi0") ls1c_spi_bus_register(LS1C_SPI_1, LS1C_SPI0_BUS_NAME);
挂接SPI从设备到SPI总线
函数原型
rt_err_t rt_spi_bus_attach_device(struct rt_spi_device *device, const char *name, const char *bus_name, void *user_data)
这个直接就是RTT的统一标准接口rt_spi_bus_attach_device()。
使用示例
#define LS1C_SPI0_BUS_NAME ("spi0") #define TM7705_SPI_DEVICE_NAME ("tm7705") struct rt_spi_device tm7705_spi_device; static struct ls1c_spi_cs spi_cs; // 把从设备(tm7705)挂在SPI总线上 spi_cs.cs = LS1C_SPI_CS_1; result = rt_spi_bus_attach_device(&tm7705_spi_device, TM7705_SPI_DEVICE_NAME, LS1C_SPI0_BUS_NAME, &spi_cs); if (RT_EOK != result) { rt_kprintf("[%s] attach spi device tm7705 fail.\n", __FUNCTION__); return ; }
注意,这里把片选信息以入参的形式传递给RTT。变量tm7705_spi_device和spi_cs常定义为static类型,我这里把tm7705_spi_device定义为全局变量了,也是可以的。目的是不能把这两个变量作为函数的临时变量,所占的空间不能被自动释放。
片选信息的结构体定义在头文件“bsp\ls1cdev\drivers\drv_spi.h”里,如下
struct ls1c_spi_cs { unsigned char cs; // LS1C_SPI_CS_0, LS1C_SPI_CS_1, LS1C_SPI_CS_2 or LS1C_SPI_CS_3 };
片选的几个可选值在头文件“bsp\ls1cdev\libraries\ls1c_spi.h”中已定义好了,如下
// 片选 #define LS1C_SPI_CS_0 (0) #define LS1C_SPI_CS_1 (1) #define LS1C_SPI_CS_2 (2) #define LS1C_SPI_CS_3 (3)
注意,这几个片选宏的值不能随意更改,函数ls1c_spi_set_cs()使用这几个值来设置龙芯1C的SPI片选控制寄存器的。实际上也没必要修改这几个宏的值,直接调用即可。
配置SPI
函数原型
t_err_t rt_spi_configure(struct rt_spi_device *device, struct rt_spi_configuration *cfg)
配置信息的结构体为
/** * SPI configuration structure */ struct rt_spi_configuration { rt_uint8_t mode; rt_uint8_t data_width; rt_uint16_t reserved; rt_uint32_t max_hz; };
注意,龙芯1C只关注其中的mode和max_hz就可以了。data_width和reserved可以忽略。
使用示例
struct rt_spi_device tm7705_spi_device; struct rt_spi_configuration cfg; // 配置SPI cfg.mode = RT_SPI_MODE_3; cfg.max_hz = 100*1000; rt_spi_configure(&tm7705_spi_device, &cfg);
几个常用的SPI收发函数
rt_spi_send()
函数原型
rt_inline rt_size_t rt_spi_send(struct rt_spi_device *device, const void *send_buf, rt_size_t length)
使用示例
struct rt_spi_device tm7705_spi_device; const unsigned char send_buf[4] = {0xFF}; rt_spi_send(&tm7705_spi_device, send_buf, 4);
rt_spi_recv()
函数原型
rt_inline rt_size_t rt_spi_recv(struct rt_spi_device *device, void *recv_buf, rt_size_t length)
使用示例
struct rt_spi_device tm7705_spi_device; Unsigned char recv_buf[4] = {0}; rt_spi_recv(&tm7705_spi_device, recv_buf, 4);
rt_spi_send_then_recv()
函数原型
rt_err_t rt_spi_send_then_recv(struct rt_spi_device *device, const void *send_buf, rt_size_t send_length, void *recv_buf, rt_size_t recv_length)
使用示例
struct rt_spi_device tm7705_spi_device; unsigned char send_buf[1] = {0}; unsigned char recv_buf[2] = {0}; rt_spi_send_then_recv(&tm7705_spi_device, send_buf, 1, recv_buf, 2);
RTT还提供了rt_spi_send_then_send()、rt_spi_transfer()等,具体查看源码,应该很容易看懂的。
设置引脚复用(不是必须的)
可能需要设置引脚复用,引脚复用接口使用头文件“bsp\ls1cdev\libraries\ls1c_pin.h”中的pin_set_remap()即可。例如
// spi复用 #define LS1C_SPI_1_CS_0_GPIO (49) // gpio49/spi1_cs0/CAMHSYNC #define LS1C_SPI_1_CS_1_GPIO (50) // gpio50/spi1_cs1/CAMDATA0 #define LS1C_SPI_1_CS_2_GPIO (51) // gpio51/spi1_cs2/CAMDATA1 #define LS1C_SPI_1_CS_3_GPIO (52) // gpio52/spi1_cs3/CAMDATA2 #define LS1C_SPI_1_MISO_GPIO (47) // gpio47/spi1_miso/CAMCLKOUT #define LS1C_SPI_1_MOSI_GPIO (48) // gpio48/spi1_mosi/CAMVSYNC #define LS1C_SPI_1_CLK_GPIO (46) // gpio46/spi1_clk/CAMPCLKIN // SPI1 CS1 pin_set_remap(LS1C_SPI_1_MISO_GPIO, PIN_REMAP_THIRD); pin_set_remap(LS1C_SPI_1_MOSI_GPIO, PIN_REMAP_THIRD); pin_set_remap(LS1C_SPI_1_CLK_GPIO, PIN_REMAP_THIRD); pin_set_remap(LS1C_SPI_1_CS_1_GPIO, PIN_REMAP_THIRD); // cs1
综合应用示例——在龙芯1C上接双路16位AD芯片TM7705
本示例为3d打印机中使用tm7705+NTC热敏电阻实现温度测量的那部分代码,有兴趣的可以移步到
《【龙印】在龙芯1c上用TM7705+NTC热敏电阻实现温度测量》http://blog.csdn.net/caogos/article/details/53126628
《【龙印】龙芯1c上双路16位AD芯片TM7705的linux驱动》http://blog.csdn.net/caogos/article/details/53034196
这里直接贴源码了。
application.c
bsp\ls1cdev\applications\application.c/* * File : application.c * This file is part of RT-Thread RTOS * COPYRIGHT (C) 2006-2012, RT-Thread Develop Team * * The license and distribution terms for this file may be * found in the file LICENSE in this distribution or at * http://www.rt-thread.org/license/LICENSE * * Change Logs: * Date Author Notes * 2010-06-25 Bernard first version * 2011-08-08 lgnq modified for Loongson LS1B * 2015-07-06 chinesebear modified for Loongson LS1C */ #include <rtthread.h> #include "net/synopGMAC.h" #include <lwip/api.h> #include "test_spi.h" // 测试用的线程 #define THREAD_TEST_PRIORITY (25) #define THREAD_TEST_STACK_SIZE (4*1024) // 4k #define THREAD_TEST_TIMESLICE (10) struct rt_thread thread_test; ALIGN(8) rt_uint8_t thread_test_stack[THREAD_TEST_STACK_SIZE]; // 测试用的线程的入口 void thread_test_entry(void *parameter) { float temp_f; // tm7705初始化 tm7705_init(); rt_kprintf("[%s] tm7705 init ok!\r\n", __FUNCTION__); while (1) { temp_f = tm7705_get_temperature(); rt_thread_delay(2*1000); } } void rt_init_thread_entry(void *parameter) { /* initialization RT-Thread Components */ rt_components_init(); // 网口EMAC初始化 rt_hw_eth_init(); } int rt_application_init(void) { rt_thread_t tid; rt_err_t result; /* create initialization thread */ tid = rt_thread_create("init", rt_init_thread_entry, RT_NULL, 4096, RT_THREAD_PRIORITY_MAX/3, 20); if (tid != RT_NULL) rt_thread_startup(tid); // 初始化测试用的线程 result = rt_thread_init(&thread_test, "thread_test", thread_test_entry, RT_NULL, &thread_test_stack[0], sizeof(thread_test_stack), THREAD_TEST_PRIORITY, THREAD_TEST_TIMESLICE); if (RT_EOK == result) { rt_thread_startup(&thread_test); } else { return -1; } return 0; }
test_spi.c
bsp\ls1cdev\applications\test_spi.c
// 测试硬件spi源文件 #include <rtthread.h> #include <drivers/spi.h> #include "../libraries/ls1c_public.h" #include "../libraries/ls1c_pin.h" #include "../libraries/ls1c_gpio.h" #include "../libraries/ls1c_delay.h" #include "../drivers/drv_spi.h" // spi复用 #define LS1C_SPI_1_CS_0_GPIO (49) // gpio49/spi1_cs0/CAMHSYNC #define LS1C_SPI_1_CS_1_GPIO (50) // gpio50/spi1_cs1/CAMDATA0 #define LS1C_SPI_1_CS_2_GPIO (51) // gpio51/spi1_cs2/CAMDATA1 #define LS1C_SPI_1_CS_3_GPIO (52) // gpio52/spi1_cs3/CAMDATA2 #define LS1C_SPI_1_MISO_GPIO (47) // gpio47/spi1_miso/CAMCLKOUT #define LS1C_SPI_1_MOSI_GPIO (48) // gpio48/spi1_mosi/CAMVSYNC #define LS1C_SPI_1_CLK_GPIO (46) // gpio46/spi1_clk/CAMPCLKIN // 通信寄存器bit定义 enum { // 寄存器选择 RS2 RS1 RS0 TM7705_REG_COMM = (0 << 4), // 通信寄存器 TM7705_REG_SETUP = (1 << 4), // 设置寄存器 TM7705_REG_CLOCK = (2 << 4), // 时钟寄存器 TM7705_REG_DATA = (3 << 4), // 数据寄存器 TM7705_REG_TEST = (4 << 4), // 测试寄存器 TM7705_REG_OFFSET = (6 << 4), // 偏移寄存器 TM7705_REG_GAIN = (7 << 4), // 增益寄存器 // 读写操作 TM7705_WRITE = (0 << 3), // 写操作 TM7705_READ = (1 << 3), // 读操作 // 通道 TM7705_CH_1 = 0, // AIN1+ AIN1- TM7705_CH_2 = 1, // AIN2+ AIN2- TM7705_CH_3 = 2, // AIN1- AIN1- TM7705_CH_4 = 3 // AIN1- AIN2- }; /* 设置寄存器bit定义 */ enum { TM7705_MD_NORMAL = (0 << 6), /* 正常模式 */ TM7705_MD_CAL_SELF = (1 << 6), /* 自校准模式 */ TM7705_MD_CAL_ZERO = (2 << 6), /* 校准0刻度模式 */ TM7705_MD_CAL_FULL = (3 << 6), /* 校准满刻度模式 */ TM7705_GAIN_1 = (0 << 3), /* 增益 */ TM7705_GAIN_2 = (1 << 3), /* 增益 */ TM7705_GAIN_4 = (2 << 3), /* 增益 */ TM7705_GAIN_8 = (3 << 3), /* 增益 */ TM7705_GAIN_16 = (4 << 3), /* 增益 */ TM7705_GAIN_32 = (5 << 3), /* 增益 */ TM7705_GAIN_64 = (6 << 3), /* 增益 */ TM7705_GAIN_128 = (7 << 3), /* 增益 */ /* 无论双极性还是单极性都不改变任何输入信号的状态,它只改变输出数据的代码和转换函数上的校准点 */ TM7705_BIPOLAR = (0 << 2), /* 双极性输入 */ TM7705_UNIPOLAR = (1 << 2), /* 单极性输入 */ TM7705_BUF_NO = (0 << 1), /* 输入无缓冲(内部缓冲器不启用) */ TM7705_BUF_EN = (1 << 1), /* 输入有缓冲 (启用内部缓冲器) */ TM7705_FSYNC_0 = 0, // 模拟调制器和滤波器正常处理数据 TM7705_FSYNC_1 = 1 // 模拟调制器和滤波器不启用 }; /* 时钟寄存器bit定义 */ enum { TM7705_CLKDIS_0 = (0 << 4), /* 时钟输出使能 (当外接晶振时,必须使能才能振荡) */ TM7705_CLKDIS_1 = (1 << 4), /* 时钟禁止 (当外部提供时钟时,设置该位可以禁止MCK_OUT引脚输出时钟以省电 */ TM7705_CLKDIV_0 = (0 << 3), // 不分频 TM7705_CLKDIV_1 = (1 << 3), // 2分频,外部晶振为4.9152Mhz时,应2分频 TM7705_CLK_0 = (0 << 2), // 主时钟=1Mhz并且CLKDIV=0,主时钟=2Mhz并且CLKDIV=1 TM7705_CLK_1 = (1 << 2), // 主时钟=2.4576Mhz并且CLKDIV=0, 主时钟=4.9152Mhz并且CLKDIV=1 // 注意输出更新率与clk位有关 // 当TM7705_CLK_0时,输出更新率只能为20,25,100,200 TM7705_UPDATE_20 = (0), TM7705_UPDATE_25 = (1), TM7705_UPDATE_100 = (2), TM7705_UPDATE_200 = (3), // 当TM7705_CLK_1时,输出更新率只能为50,60,250,500 TM7705_UPDATE_50 = (0), TM7705_UPDATE_60 = (1), TM7705_UPDATE_250 = (2), TM7705_UPDATE_500 = (3) }; #define TM7705_CHANNEL_NUM (2) // tm7705通道个数 #define TM7705_DRDY_PIN (87) // GPIO87/I2S_DI tm7705的引脚DRDY #define TM7705_RESET_PIN (89) // GPIO89/I2S_LRCK tm7705的引脚RESET #define TM7705_AD_MAX ((0x1 << 10) - 1) // ad的最大值,只用了十位的精度 #define TM7705_IS_VALID_AD(ad) ((TM7705_AD_MAX >= (ad)) && (0 <= (ad))) // 返回值 #define TM7705_RET_TIMEOUT (-1) // 超时 #define TM7705_RET_OK (0) // 正常返回 #define TM7705_RET_OTHER_ERR (1) // 其它错误 #define LS1C_SPI0_BUS_NAME ("spi0") #define TM7705_SPI_DEVICE_NAME ("tm7705") // 以下根据ntc热敏电阻参数用脚本生成的adc值与温度一一对应的表格 // 左边为adc值,右边为温度(单位:摄氏度) // 详细请参考源码目录中的脚本"createTemperatureLookup.py" // python createTemperatureLookup.py // Thermistor lookup table for RepRap Temperature Sensor Boards (http://make.rrrf.org/ts) // Made with createTemperatureLookup.py (http://svn.reprap.org/trunk/reprap/firmware/Arduino/utilities/createTemperatureLookup.py) // ./createTemperatureLookup.py --r0=100000 --t0=25 --r1=0 --r2=4700 --beta=3950 --max-adc=1023 // r0: 100000 // t0: 25 // r1: 0 // r2: 4700 // beta: 3950 // max adc: 1023 #define TM7705_NTC_NUMTEMPS 40 const short tm7705_ntc_temptable[TM7705_NTC_NUMTEMPS][2] = { {1, 938}, {27, 326}, {53, 269}, {79, 239}, {105, 219}, {131, 204}, {157, 192}, {183, 182}, {209, 174}, {235, 166}, {261, 160}, {287, 153}, {313, 148}, {339, 143}, {365, 138}, {391, 133}, {417, 129}, {443, 125}, {469, 120}, {495, 116}, {521, 113}, {547, 109}, {573, 105}, {599, 101}, {625, 98}, {651, 94}, {677, 90}, {703, 86}, {729, 82}, {755, 78}, {781, 74}, {807, 70}, {833, 65}, {859, 60}, {885, 54}, {911, 48}, {937, 41}, {963, 31}, {989, 18}, {1015, -8} }; struct rt_spi_device tm7705_spi_device; /* * 通过RESET引脚复位tm7705 */ void tm7705_reset(void) { gpio_set(TM7705_RESET_PIN, gpio_level_high); delay_ms(1); gpio_set(TM7705_RESET_PIN, gpio_level_low); delay_ms(2); gpio_set(TM7705_RESET_PIN, gpio_level_high); delay_ms(1); return ; } /* * 同步SPI接口时序 */ void tm7705_sync_spi(void) { const unsigned char send_buf[4] = {0xFF}; // 在至少32个串行时钟内向TM7705的DIN脚写入逻辑'1' rt_spi_send(&tm7705_spi_device, send_buf, 4); return ; } /* * 等待内部操作完成, 时间较长,约180ms * @ret 成功 或者 超时 */ int tm7705_wait_DRDY(void) { int i = 0; int time_cnt = 500; for (i=0; i<time_cnt; i++) { if (gpio_level_low == gpio_get(TM7705_DRDY_PIN)) { break; } delay_ms(1); } if (i >= time_cnt) { return TM7705_RET_TIMEOUT; } return TM7705_RET_OK; } /* * 自校准 * @channel 通道 */ void tm7705_calib_self(unsigned char channel) { unsigned char send_buf[2] = {0}; send_buf[0] = TM7705_REG_SETUP | TM7705_WRITE | channel; send_buf[1] = TM7705_MD_CAL_SELF | TM7705_GAIN_1 | TM7705_UNIPOLAR | TM7705_BUF_EN | TM7705_FSYNC_0; rt_spi_send(&tm7705_spi_device, send_buf, 2); // 等待内部操作完成, 时间较长,约180ms tm7705_wait_DRDY(); delay_ms(50); return ; } /* * 配置tm7705的指定通道 * @channel 通道 */ void tm7705_config_channel(unsigned char channel) { unsigned char send_buf[2] = {0}; send_buf[0] = TM7705_REG_CLOCK | TM7705_WRITE | channel; send_buf[1] = TM7705_CLKDIS_0 | TM7705_CLKDIV_1 | TM7705_CLK_1 | TM7705_UPDATE_50; rt_spi_send(&tm7705_spi_device, send_buf, 2); // 自校准 tm7705_calib_self(channel); return ; } /* * 复位tm7705并重新配置 */ void tm7705_reset_and_reconfig(void) { // 通过RESET引脚复位tm7705 tm7705_reset(); // 同步SPI接口时序 delay_ms(5); tm7705_sync_spi(); delay_ms(5); // 配置tm7705的指定通道 tm7705_config_channel(TM7705_CH_1); return ; } /* * tm7705初始化 */ void tm7705_init(void) { static struct ls1c_spi_cs spi_cs; struct rt_spi_configuration cfg; rt_err_t result; // 初始化DRDY和RESET引脚 gpio_init(TM7705_DRDY_PIN, gpio_mode_input); gpio_init(TM7705_RESET_PIN, gpio_mode_output); gpio_set(TM7705_RESET_PIN, gpio_level_high); // 注册SPI总线 ls1c_spi_bus_register(LS1C_SPI_1, LS1C_SPI0_BUS_NAME); // ls1c_spi_bus_register(LS1C_SPI_0, LS1C_SPI0_BUS_NAME); // 把从设备(tm7705)挂在SPI总线上 spi_cs.cs = LS1C_SPI_CS_1; result = rt_spi_bus_attach_device(&tm7705_spi_device, TM7705_SPI_DEVICE_NAME, LS1C_SPI0_BUS_NAME, &spi_cs); if (RT_EOK != result) { rt_kprintf("[%s] attach spi device tm7705 fail.\n", __FUNCTION__); return ; } // 配置SPI cfg.mode = RT_SPI_MODE_3; cfg.max_hz = 100*1000; rt_spi_configure(&tm7705_spi_device, &cfg); // 复用 // 下面我选择了几路spi进行测试,都通过了的,有需要的可以把注释取消,进行测试 /* // SPI1 CS0 pin_set_remap(LS1C_SPI_1_MISO_GPIO, PIN_REMAP_THIRD); pin_set_remap(LS1C_SPI_1_MOSI_GPIO, PIN_REMAP_THIRD); pin_set_remap(LS1C_SPI_1_CLK_GPIO, PIN_REMAP_THIRD); pin_set_remap(LS1C_SPI_1_CS_0_GPIO, PIN_REMAP_THIRD); // cs0 */ // SPI1 CS1 pin_set_remap(LS1C_SPI_1_MISO_GPIO, PIN_REMAP_THIRD); pin_set_remap(LS1C_SPI_1_MOSI_GPIO, PIN_REMAP_THIRD); pin_set_remap(LS1C_SPI_1_CLK_GPIO, PIN_REMAP_THIRD); pin_set_remap(LS1C_SPI_1_CS_1_GPIO, PIN_REMAP_THIRD); // cs1 // 复位tm7705并重新配置 tm7705_reset_and_reconfig(); return ; } /* * 读取指定通道的ad值 * @channel 通道 * @adc_p 读取的AD值 * @ret 成功 或 失败 */ int tm7705_read_channel(int channel, unsigned short *adc_p) { int ret = TM7705_RET_TIMEOUT; unsigned char send_buf[1] = {0}; unsigned char recv_buf[2] = {0}; unsigned short ad = 0; // 等待转换完成 ret = tm7705_wait_DRDY(); if (TM7705_RET_OK != ret) { rt_kprintf("[%s] tm7705 timeout!\r\n", __FUNCTION__); return ret; } // 读 send_buf[0] = TM7705_REG_DATA | TM7705_READ | channel; rt_spi_send_then_recv(&tm7705_spi_device, send_buf, 1, recv_buf, 2); ad = (recv_buf[0] << 8) + recv_buf[1]; if (0xfff == ad) { rt_kprintf("[%s] ad=0xfff\r\n", __FUNCTION__); return TM7705_RET_OTHER_ERR; } *adc_p = ad; return TM7705_RET_OK; } /* * 获取tm7705输出的AD值 * @channel 通道 * @adc_p 读取的AD值 * @ret 成功 或者 失败 */ int tm7705_get_ad(int channel, unsigned short *adc_p) { int i = 0; int ret = TM7705_RET_TIMEOUT; unsigned short ad = 0; // 连续读2次 // 第一次读取的值为上一次采集的结果,第二次读取的才是当前的结果 * for (i=0; i<2; i++) { ret = tm7705_read_channel(channel, &ad); if (TM7705_RET_OK != ret) { // 失败,则重启tm7705并重新配置 tm7705_reset_and_reconfig(); rt_kprintf("[%s] tm7705 reset and reconfig!\r\n", __FUNCTION__); return ret; } // ls1c速度相对tm7705太快,延时一下避免在一次读完后DRDY还未及时改变状态 delay_ms(1); } // spi_print_all_regs_info(&tm7705_spi_info); rt_kprintf("[%s] ad=0x%x\r\n", __FUNCTION__, ad); ad = ad >> 6; // 只需要10位的精度 *adc_p = ad; return TM7705_RET_OK; } /* * 根据ad值计算温度值 * @ad 从tm7705读取的AD值 * @ret 温度值 * * ntc热敏电阻的阻值温度曲线被分为n段,每段可以近似为直线 * 所以温度值的计算就转变为查表再计算 */ float tm7705_calc_temp_from_ad(unsigned short ad) { float temp_f; int i = 0; // 判断ad值是否在量程范围内 if (!(TM7705_IS_VALID_AD(ad))) { return 0; } // 判断是否在表格所表示的范围内 if (ad < tm7705_ntc_temptable[0][0]) // 小于表格的最小adc { return tm7705_ntc_temptable[0][1]; // 取最小值 } if (ad > tm7705_ntc_temptable[TM7705_NTC_NUMTEMPS-1][0]) // 大于表格的最大adc { return tm7705_ntc_temptable[TM7705_NTC_NUMTEMPS-1][1]; // 取最大值 } // 查表 for (i=1; i<TM7705_NTC_NUMTEMPS; i++) { if (ad < tm7705_ntc_temptable[i][0]) { // rt_kprintf("[%s] ad=%d\r\n", __FUNCTION__, ad); // t = t0 + (adc - adc0) * k temp_f = tm7705_ntc_temptable[i-1][1] + // t0 (ad - tm7705_ntc_temptable[i-1][0]) * // adc - adc0 ((float)(tm7705_ntc_temptable[i][1] - tm7705_ntc_temptable[i-1][1]) / (float)(tm7705_ntc_temptable[i][0] - tm7705_ntc_temptable[i-1][0])); rt_kprintf("[%s] temp_f=%d\r\n", __FUNCTION__, (int)temp_f); return temp_f; } } return 0; } /* * 获取温度值 * @ret 温度值 */ float tm7705_get_temperature(void) { int ret = TM7705_RET_TIMEOUT; float temp_f = 0; unsigned short ad = 0; // 获取AD值 ret = tm7705_get_ad(TM7705_CH_1, &ad); if (TM7705_RET_OK != ret) { rt_kprintf("[%s] tm7705_get_ad() fail, ret=%d\r\n", __FUNCTION__, ret); return ret; } // 计算温度值 temp_f = tm7705_calc_temp_from_ad(ad); return temp_f; }
把RTT统一的标准的SPI接口移植到龙芯1C上
这一部分主要讨论一下移植的具体细节和思路。整个移植最核心的也就是两个函数configure()和xfer(),这两个函数为struct rt_spi_ops的成员,也是RTT的SPI接口的核心所在。
在龙芯1c库的SPI接口中,已经实现了配置和收发功能,只是配置功能被拆分为多个函数而已。比如设置时钟调用ls1c_spi_set_clock(),设置spi模式调用ls1c_spi_set_mode()等。
源码行数不多,相信大家一看就能明白,这里直接贴源码了
drv_spi.c
bsp\ls1cdev\drivers\drv_spi.c
/* * File : drv_spi.c * This file is part of RT-Thread RTOS * COPYRIGHT (C) 2006 - 2012, RT-Thread Development Team * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * Change Logs: * Date Author Notes * 2017-11-02 勤为本 first version */ #include <rtthread.h> #include <drivers/spi.h> #include "drv_spi.h" //#define DEBUG #ifdef DEBUG #define DEBUG_PRINTF(...) rt_kprintf(__VA_ARGS__) #else #define DEBUG_PRINTF(...) #endif static rt_err_t configure(struct rt_spi_device *device, struct rt_spi_configuration *configuration); static rt_uint32_t xfer(struct rt_spi_device *device, struct rt_spi_message *message); static struct rt_spi_ops ls1c_spi_ops = { .configure = configure, .xfer = xfer }; static rt_err_t configure(struct rt_spi_device *device, struct rt_spi_configuration *configuration) { struct rt_spi_bus *spi_bus = NULL; struct ls1c_spi *ls1c_spi = NULL; unsigned char SPIx = 0; void *spi_base = NULL; unsigned char cpol = 0; unsigned char cpha = 0; unsigned char val = 0; RT_ASSERT(NULL != device); RT_ASSERT(NULL != configuration); spi_bus = device->bus; ls1c_spi = (struct ls1c_spi *)spi_bus->parent.user_data; SPIx = ls1c_spi->SPIx; spi_base = ls1c_spi_get_base(SPIx); { // 使能SPI控制器,master模式,关闭中断 reg_write_8(0x53, spi_base + LS1C_SPI_SPCR_OFFSET); // 清空状态寄存器 reg_write_8(0xc0, spi_base + LS1C_SPI_SPSR_OFFSET); // 1字节产生中断,采样(读)与发送(写)时机同时 reg_write_8(0x03, spi_base + LS1C_SPI_SPER_OFFSET); // 关闭SPI flash val = reg_read_8(spi_base + LS1C_SPI_SFC_PARAM_OFFSET); val &= 0xfe; reg_write_8(val, spi_base + LS1C_SPI_SFC_PARAM_OFFSET); // spi flash时序控制寄存器 reg_write_8(0x05, spi_base + LS1C_SPI_SFC_TIMING_OFFSET); } // baudrate ls1c_spi_set_clock(spi_base, configuration->max_hz); // 设置通信模式(时钟极性和相位) if (configuration->mode & RT_SPI_CPOL) // cpol { cpol = SPI_CPOL_1; } else { cpol = SPI_CPOL_0; } if (configuration->mode & RT_SPI_CPHA) // cpha { cpha = SPI_CPHA_1; } else { cpha = SPI_CPHA_0; } ls1c_spi_set_mode(spi_base, cpol, cpha); DEBUG_PRINTF("ls1c spi%d configuration\n", SPIx); return RT_EOK; } static rt_uint32_t xfer(struct rt_spi_device *device, struct rt_spi_message *message) { struct rt_spi_bus *spi_bus = NULL; struct ls1c_spi *ls1c_spi = NULL; void *spi_base = NULL; unsigned char SPIx = 0; struct ls1c_spi_cs *ls1c_spi_cs = NULL; unsigned char cs = 0; rt_uint32_t size = 0; const rt_uint8_t *send_ptr = NULL; rt_uint8_t *recv_ptr = NULL; rt_uint8_t data = 0; RT_ASSERT(NULL != device); RT_ASSERT(NULL != message); spi_bus = device->bus; ls1c_spi = spi_bus->parent.user_data; SPIx = ls1c_spi->SPIx; spi_base = ls1c_spi_get_base(SPIx); ls1c_spi_cs = device->parent.user_data; cs = ls1c_spi_cs->cs; size = message->length; DEBUG_PRINTF("[%s] SPIx=%d, cs=%d\n", __FUNCTION__, SPIx, cs); // take cs if (message->cs_take) { ls1c_spi_set_cs(spi_base, cs, 0); } // 收发数据 send_ptr = message->send_buf; recv_ptr = message->recv_buf; while (size--) { data = 0xFF; if (NULL != send_ptr) { data = *send_ptr++; } if (NULL != recv_ptr) { *recv_ptr++ = ls1c_spi_txrx_byte(spi_base, data); } else { ls1c_spi_txrx_byte(spi_base, data); } } // release cs if (message->cs_release) { ls1c_spi_set_cs(spi_base, cs, 1); } return message->length; } #ifdef RT_USING_SPI0 struct ls1c_spi ls1c_spi0 = { .SPIx = LS1C_SPI_0, }; static struct rt_spi_bus spi0_bus; #endif #ifdef RT_USING_SPI1 struct ls1c_spi ls1c_spi1 = { .SPIx = LS1C_SPI_1, }; static struct rt_spi_bus spi1_bus; #endif /* * 初始化并注册龙芯1c的spi总线 * @SPI SPI总线,比如LS1C_SPI_0, LS1C_SPI_1 * @spi_bus_name 总线名字 * @ret */ rt_err_t ls1c_spi_bus_register(rt_uint8_t SPI, const char *spi_bus_name) { struct rt_spi_bus *spi_bus = NULL; #ifdef RT_USING_SPI0 if (LS1C_SPI_0 == SPI) { spi_bus = &spi0_bus; spi_bus->parent.user_data = &ls1c_spi0; } #endif #ifdef RT_USING_SPI1 if (LS1C_SPI_1 == SPI) { spi_bus = &spi1_bus; spi_bus->parent.user_data = &ls1c_spi1; } #endif return rt_spi_bus_register(spi_bus, spi_bus_name, &ls1c_spi_ops); }
drv_spi.h
bsp\ls1cdev\drivers\drv_spi.h
/* * File : drv_spi.h * This file is part of RT-Thread RTOS * COPYRIGHT (C) 2006 - 2012, RT-Thread Development Team * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * Change Logs: * Date Author Notes * 2017-11-02 勤为本 first version */ #ifndef LS1C_DRV_SPI_H #define LS1C_DRV_SPI_H #include "../libraries/ls1c_spi.h" struct ls1c_spi { unsigned char SPIx; // LS1C_SPI_0 or LS1C_SPI_1 }; struct ls1c_spi_cs { unsigned char cs; // LS1C_SPI_CS_0, LS1C_SPI_CS_1, LS1C_SPI_CS_2 or LS1C_SPI_CS_3 }; /* * 初始化并注册龙芯1c的spi总线 * @SPI SPI总线,比如LS1C_SPI_0, LS1C_SPI_1 * @spi_bus_name 总线名字 * @ret */ rt_err_t ls1c_spi_bus_register(rt_uint8_t SPI, const char *spi_bus_name); #endif
ls1c_spi.c
bsp\ls1cdev\libraries\ls1c_spi.c
/* * File : ls1c_spi.c * This file is part of RT-Thread RTOS * COPYRIGHT (C) 2006 - 2012, RT-Thread Development Team * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * Change Logs: * Date Author Notes * 2017-10-23 勤为本 first version */ // 硬件spi接口源文件 #include <string.h> #include "ls1c_public.h" #include "ls1c_regs.h" #include "ls1c_clock.h" #include "ls1c_spi.h" /* * 获取指定SPI模块的基地址 * @SPIx SPI模块的编号 */ inline void *ls1c_spi_get_base(unsigned char SPIx) { void *base = NULL; switch (SPIx) { case LS1C_SPI_0: base = (void *)LS1C_SPI0_BASE; break; case LS1C_SPI_1: base = (void *)LS1C_SPI1_BASE; break; default: base = NULL; break; } return base; } /* * 打印指定SPI模块的所有寄存器的值 * @spi_base 基地址 */ void ls1c_spi_print_all_regs_info(void *spi_base) { rt_kprintf("[%s] SPCR=0x%x, SPSR=0x%x, SPER=0x%x, SFC_PARAM=0x%x, SFC_SOFTCS=0x%x, SFC_TIMING=0x%x\r\n", __FUNCTION__, reg_read_8(spi_base + LS1C_SPI_SPCR_OFFSET), reg_read_8(spi_base + LS1C_SPI_SPSR_OFFSET), reg_read_8(spi_base + LS1C_SPI_SPER_OFFSET), reg_read_8(spi_base + LS1C_SPI_SFC_PARAM_OFFSET), reg_read_8(spi_base + LS1C_SPI_SFC_SOFTCS_OFFSET), reg_read_8(spi_base + LS1C_SPI_SFC_TIMING_OFFSET)); return ; } /* * 根据SPI时钟频率计算分频系数 * @max_speed_hz SPI最大通信速度 * @ret 分频系数 */ unsigned int ls1c_spi_get_div(unsigned int max_speed_hz) { unsigned long clk = 0; unsigned int div = 0; unsigned int div_tmp = 0; unsigned int bit = 0; clk = clk_get_apb_rate(); div = DIV_ROUND_UP(clk, max_speed_hz); if (div < 2) div = 2; if (div > 4096) div = 4096; bit = ls1c_fls(div) - 1; switch (1 << bit) { case 16: div_tmp = 2; if (div > (1 << bit)) { div_tmp++; } break; case 32: div_tmp = 3; if (div > (1 << bit)) { div_tmp += 2; } break; case 8: div_tmp = 4; if (div > (1 << bit)) { div_tmp -= 2; } break; default: div_tmp = bit - 1; if (div > (1 << bit)) { div_tmp++; } break; } /* rt_kprintf("[%s] clk=%ld, max_speed_hz=%d, div_tmp=%d, bit=%d\r\n", __FUNCTION__, clk, max_speed_hz, div_tmp, bit); */ return div_tmp; } /* * 设置时钟 * @spi_base 基地址 * @max_hz 最大频率,单位hz */ void ls1c_spi_set_clock(void *spi_base, unsigned long max_hz) { unsigned int div = 0; unsigned char val = 0; // 获取分频系数 div = ls1c_spi_get_div(max_hz); // 设置spr val = reg_read_8(spi_base + LS1C_SPI_SPCR_OFFSET); val &= (~LS1C_SPI_SPCR_SPR_MASK); // spr清零 val |= (div & LS1C_SPI_SPCR_SPR_MASK); // 设置新的spr reg_write_8(val, spi_base + LS1C_SPI_SPCR_OFFSET); // 设置spre val = reg_read_8(spi_base + LS1C_SPI_SPER_OFFSET); val &= (~LS1C_SPI_SPER_SPRE_MASK); // spre清零 val |= ((div >> 2) & LS1C_SPI_SPER_SPRE_MASK); // 设置新的spre reg_write_8(val, spi_base + LS1C_SPI_SPER_OFFSET); return ; } /* * 设置通信模式(时钟极性和相位) * @spi_base 基地址 * @cpol 时钟极性 * @cpha 时钟相位 */ void ls1c_spi_set_mode(void *spi_base, unsigned char cpol, unsigned char cpha) { unsigned char val = 0; val = reg_read_8(spi_base + LS1C_SPI_SPCR_OFFSET); // 设置时钟极性--cpol val &= (~LS1C_SPI_SPCR_CPOL_MASK); // cpol清0 val |= (cpol << LS1C_SPI_SPCR_CPOL_BIT); // 写入新的cpol // 设置时钟相位--cpha val &= (~LS1C_SPI_SPCR_CPHA_MASK); // cpha清0 val |= (cpha << LS1C_SPI_SPCR_CPHA_BIT); // 写入新的cpha reg_write_8(val, spi_base + LS1C_SPI_SPCR_OFFSET); return ; } /* * 设置指定片选为指定状态 * @spi_base 基地址 * @cs 片选 * @new_status 片选引脚的新状态,取值为0或1,即高电平或低电平 */ void ls1c_spi_set_cs(void *spi_base, unsigned char cs, int new_status) { unsigned char val = 0; val = 0xf0 | (0x01 << cs); // 全部csn=1,指定的csen=1 if (new_status) // cs = 1 { val |= (0x10 << cs); // 指定csn=1 } else // cs = 0 { val &= ~(0x10 << cs); // 指定csn=0 } reg_write_8(val, spi_base + LS1C_SPI_SFC_SOFTCS_OFFSET); return ; } /* * 等待收发完成 * @spi_base 基地址 */ inline void ls1c_spi_wait_txrx_done(void *spi_base) { int timeout = LS1C_SPI_TX_TIMEOUT; while (timeout--) { if (LS1C_SPI_SPSR_SPIF_MASK & reg_read_8(spi_base + LS1C_SPI_SPSR_OFFSET)) break; } return ; } /* * 清中断和标志位 * @spi_base 基地址 */ inline void ls1c_spi_clear(void *spi_base) { unsigned char val = 0; // 清中断 val = reg_read_8(spi_base + LS1C_SPI_SPSR_OFFSET); val |= LS1C_SPI_SPSR_SPIF_MASK; reg_write_8(val, spi_base + LS1C_SPI_SPSR_OFFSET); // 清溢出标志位(Write-Collision Clear) val = reg_read_8(spi_base + LS1C_SPI_SPSR_OFFSET); if (LS1C_SPI_SPSR_WCOL_MASK & val) { rt_kprintf("[%s] clear register SPSR's wcol!\r\n"); // 手册和linux源码中不一样,加个打印看看 reg_write_8(val & ~LS1C_SPI_SPSR_WCOL_MASK, spi_base + LS1C_SPI_SPSR_OFFSET); // 写0,linux源码中是写0 // reg_write_8(val | LS1C_SPI_SPSR_WCOL_MASK, spi_base + LS1C_SPI_SPSR_OFFSET); // 写1,按照1c手册,应该写1 } return ; } /* * 通过指定SPI发送接收一个字节 * 注意,在多任务的系统中,此函数需要互斥。 * 即保证在和某个从设备收发某个字节的过程中,不能被切换到其它任务同时与另外的在同一个SPI总线上的从设备通信 * 因为龙芯1c的每路SPI上可能接有不同的从设备,通信频率、模式等可能不同 * @spi_base 基地址 * @tx_ch 待发送的数据 * @ret 收到的数据 */ unsigned char ls1c_spi_txrx_byte(void *spi_base, unsigned char tx_ch) { unsigned char rx_ch = 0; // 收发数据 reg_write_8(tx_ch, spi_base + LS1C_SPI_TxFIFO_OFFSET); // 开始发送 ls1c_spi_wait_txrx_done(spi_base); // 等待收发完成 rx_ch = reg_read_8(spi_base + LS1C_SPI_RxFIFO_OFFSET); // 读取收到的数据 ls1c_spi_clear(spi_base); // 清中断和标志位 return rx_ch; }
ls1c_spi.h
bsp\ls1cdev\libraries\ls1c_spi.h
/* * File : ls1c_spi.h * This file is part of RT-Thread RTOS * COPYRIGHT (C) 2006 - 2012, RT-Thread Development Team * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * Change Logs: * Date Author Notes * 2017-10-23 勤为本 first version */ // 硬件spi接口的头文件 #ifndef __OPENLOONGSON_SPI_H #define __OPENLOONGSON_SPI_H // SPI模块编号 #define LS1C_SPI_0 (0) #define LS1C_SPI_1 (1) // 片选 #define LS1C_SPI_CS_0 (0) #define LS1C_SPI_CS_1 (1) #define LS1C_SPI_CS_2 (2) #define LS1C_SPI_CS_3 (3) // 时钟极性和相位 #define SPI_CPOL_1 (1) #define SPI_CPOL_0 (0) #define SPI_CPHA_1 (1) #define SPI_CPHA_0 (0) // 寄存器偏移 #define LS1C_SPI_SPCR_OFFSET (0) // 控制寄存器 #define LS1C_SPI_SPSR_OFFSET (1) // 状态寄存器 #define LS1C_SPI_TxFIFO_OFFSET (2) // 发送的数据寄存器,与接收数据寄存器的偏移相同 #define LS1C_SPI_RxFIFO_OFFSET (2) // 接收的数据寄存器,与发送数据寄存器的偏移相同 #define LS1C_SPI_SPER_OFFSET (3) // 外部寄存器 #define LS1C_SPI_SFC_PARAM_OFFSET (4) // 参数控制寄存器 #define LS1C_SPI_SFC_SOFTCS_OFFSET (5) // 片选控制寄存器 #define LS1C_SPI_SFC_TIMING_OFFSET (6) // 时序控制寄存器 // 寄存器SPCR中的位域 #define LS1C_SPI_SPCR_SPIE_BIT (7) #define LS1C_SPI_SPCR_SPIE_MASK (0x01 << LS1C_SPI_SPCR_SPIE_BIT) #define LS1C_SPI_SPCR_SPE_BIT (6) #define LS1C_SPI_SPCR_SPE_MASK (0x01 << LS1C_SPI_SPCR_SPE_BIT) #define LS1C_SPI_SPCR_CPOL_BIT (3) #define LS1C_SPI_SPCR_CPOL_MASK (0x01 << LS1C_SPI_SPCR_CPOL_BIT) #define LS1C_SPI_SPCR_CPHA_BIT (2) #define LS1C_SPI_SPCR_CPHA_MASK (0x01 << LS1C_SPI_SPCR_CPHA_BIT) #define LS1C_SPI_SPCR_SPR_BIT (0) #define LS1C_SPI_SPCR_SPR_MASK (0x03 << LS1C_SPI_SPCR_SPR_BIT) // 寄存器SPSR中的位域 #define LS1C_SPI_SPSR_SPIF_BIT (7) #define LS1C_SPI_SPSR_SPIF_MASK (0x01 << LS1C_SPI_SPSR_SPIF_BIT) #define LS1C_SPI_SPSR_WCOL_BIT (6) #define LS1C_SPI_SPSR_WCOL_MASK (0x01 << LS1C_SPI_SPSR_WCOL_BIT) // 寄存器SPER中的位域 #define LS1C_SPI_SPER_SPRE_BIT (0) #define LS1C_SPI_SPER_SPRE_MASK (0x3 << LS1C_SPI_SPER_SPRE_BIT) // 寄存器SFC_SOFTCS的位域 #define LS1C_SPI_SFC_SOFTCS_CSN_BIT (4) #define LS1C_SPI_SFC_SOFTCS_CSN_MASK (0x0f << LS1C_SPI_SFC_SOFTCS_CSN_BIT) #define LS1C_SPI_SFC_SOFTCS_CSEN_BIT (0) #define LS1C_SPI_SFC_SOFTCS_CSEN_MASK (0x0f << LS1C_SPI_SFC_SOFTCS_CSEN_BIT) // 发送超时的门限值 #define LS1C_SPI_TX_TIMEOUT (20000) /* * 获取指定SPI模块的基地址 * @SPIx SPI模块的编号 */ inline void *ls1c_spi_get_base(unsigned char SPIx); /* * 设置时钟 * @spi_base 基地址 * @max_hz 最大频率,单位hz */ void ls1c_spi_set_clock(void *spi_base, unsigned long max_hz); /* * 设置通信模式(时钟极性和相位) * @spi_base 基地址 * @cpol 时钟极性 * @cpha 时钟相位 */ void ls1c_spi_set_mode(void *spi_base, unsigned char cpol, unsigned char cpha); /* * 设置指定片选为指定状态 * @spi_base 基地址 * @cs 片选 * @new_status 片选引脚的新状态,取值为0或1,即高电平或低电平 */ void ls1c_spi_set_cs(void *spi_base, unsigned char cs, int new_status); /* * 通过指定SPI发送接收一个字节 * 注意,在多任务的系统中,此函数需要互斥。 * 即保证在和某个从设备收发某个字节的过程中,不能被切换到其它任务同时与另外的在同一个SPI总线上的从设备通信 * 因为龙芯1c的每路SPI上可能接有不同的从设备,通信频率、模式等可能不同 * @spi_base 基地址 * @tx_ch 待发送的数据 * @ret 收到的数据 */ unsigned char ls1c_spi_txrx_byte(void *spi_base, unsigned char tx_ch); /* * 打印指定SPI模块的所有寄存器的值 * @spi_base 基地址 */ void ls1c_spi_print_all_regs_info(void *spi_base); #endif
感谢阅读!