Linux spi驱动分析(三)----spiddev分析
来源:互联网 发布:mac这么下载阿里旺旺 编辑:程序博客网 时间:2024/05/16 17:18
一、spidev简单介绍
如果在内核中配置spidev,会在“/dev”目录下产生设备节点,通过此节点可以操作挂载在该SPI总线上的设备,接下来将从驱动层和应用层
来分析程序。
二、spidev驱动层
2.1、驱动注册
分析一个设备驱动,一般都是从module_init和module_exit处开始,本文也不例外,程序如下:
点击(此处)折叠或打开
- #define SPIDEV_MAJOR 153 /* assigned*/
- #define N_SPI_MINORS 32 /*... upto 256 */
- static DECLARE_BITMAP(minors, N_SPI_MINORS);
- static struct spi_driver spidev_spi_driver ={
- .driver ={
- .name = "spidev",
- .owner = THIS_MODULE,
- },
- .probe = spidev_probe,
- .remove = __devexit_p(spidev_remove),
- /* NOTE: suspend/resume methods arenot necessary here.
- * We don'tdo anything except pass the requests to/from
- * the underlying controller. The refrigerator handles
- * most issues; the controller driver handles the rest.
- */
- };
- /*-------------------------------------------------------------------------*/
- static int __init spidev_init(void)
- {
- int status;
- /* Claim our 256 reserved device numbers.Then register a class
- * that will key udev/mdevto add/remove/dev nodes. Last, register
- * the driver which manages those device numbers.
- */
- BUILD_BUG_ON(N_SPI_MINORS > 256);
- status = register_chrdev(SPIDEV_MAJOR,"spi", &spidev_fops);
- if (status< 0)
- return status;
- spidev_class = class_create(THIS_MODULE,"spidev");
- if (IS_ERR(spidev_class)){
- unregister_chrdev(SPIDEV_MAJOR, spidev_spi_driver.driver.name);
- return PTR_ERR(spidev_class);
- }
- status = spi_register_driver(&spidev_spi_driver);
- if (status< 0) {
- class_destroy(spidev_class);
- unregister_chrdev(SPIDEV_MAJOR, spidev_spi_driver.driver.name);
- }
- return status;
- }
- module_init(spidev_init);
- static void __exit spidev_exit(void)
- {
- spi_unregister_driver(&spidev_spi_driver);
- class_destroy(spidev_class);
- unregister_chrdev(SPIDEV_MAJOR, spidev_spi_driver.driver.name);
- }
- module_exit(spidev_exit);
- MODULE_AUTHOR("Andrea Paterniani,");
- MODULE_DESCRIPTION("User mode SPI device interface");
- MODULE_LICENSE("GPL");
- MODULE_ALIAS("spi:spidev");
说明:
1) 在驱动注册函数中,首先注册函数操作集spidev_fops,该内容将在2.3中具体讲述。
2) 生成spidev设备类,此类的表现是在“/sys/class”目录下生成一个名为spidev目录。
3) 注册spi驱动。
4) 退出函数是注册函数的相反过程,就是释放注册函数中注册的资源。
2.2、探测和移除函数
探测函数程序如下:
点击(此处)折叠或打开
- static int __devinit spidev_probe(struct spi_device*spi)
- {
- struct spidev_data *spidev;
- int status;
- unsigned long minor;
- /* Allocate driver data*/
- spidev = kzalloc(sizeof(*spidev), GFP_KERNEL);
- if (!spidev)
- return -ENOMEM;
- /* Initialize the driver data*/
- spidev->spi= spi;
- spin_lock_init(&spidev->spi_lock);
- mutex_init(&spidev->buf_lock);
- INIT_LIST_HEAD(&spidev->device_entry);
- /* If we can allocate a minor number, hook up this device.
- * Reusing minors is fine so long as udev or mdev is working.
- */
- mutex_lock(&device_list_lock);
- minor = find_first_zero_bit(minors, N_SPI_MINORS);
- if (minor< N_SPI_MINORS){
- struct device *dev;
- spidev->devt= MKDEV(SPIDEV_MAJOR, minor);
- dev = device_create(spidev_class,&spi->dev, spidev->devt,
- spidev, "spidev%d.%d",
- spi->master->bus_num, spi->chip_select);
- status = IS_ERR(dev)? PTR_ERR(dev): 0;
- } else{
- dev_dbg(&spi->dev,"no minor number available!\n");
- status = -ENODEV;
- }
- if (status== 0){
- set_bit(minor, minors);
- list_add(&spidev->device_entry,&device_list);
- }
- mutex_unlock(&device_list_lock);
- if (status== 0)
- spi_set_drvdata(spi, spidev);
- else
- kfree(spidev);
- return status;
- }
说明:
1) 首先声明spidev_data结构体内存。
2) 初始化其成员变量。
3) 在位图中寻找第一个未被用到的位。
4) 如果位图寻找正确,创建“/dev”目录下的设备节点。
4) 将spidev结构体中的链表插入局部全局链表device_list中,以便在函数操作集中的open函数中使用
移除函数如下:
点击(此处)折叠或打开
- static int __devexit spidev_remove(struct spi_device*spi)
- {
- struct spidev_data *spidev = spi_get_drvdata(spi);
- /* make sure opson existing fds can abort cleanly */
- spin_lock_irq(&spidev->spi_lock);
- spidev->spi= NULL;
- spi_set_drvdata(spi,NULL);
- spin_unlock_irq(&spidev->spi_lock);
- /* prevent new opens*/
- mutex_lock(&device_list_lock);
- list_del(&spidev->device_entry);
- device_destroy(spidev_class, spidev->devt);
- clear_bit(MINOR(spidev->devt), minors);
- if (spidev->users== 0)
- kfree(spidev);
- mutex_unlock(&device_list_lock);
- return 0;
- }
说明:
1) 移除函数就是探测函数的相反过程,主要就是释放探测函数中申请的资源。
2.2、函数操作集spidev_fop
spidev_fop具体如下:
点击(此处)折叠或打开
- static const struct file_operations spidev_fops= {
- .owner = THIS_MODULE,
- /* REVISIT switchto aio primitives, so that userspace
- * gets more complete API coverage. It'll simplify things
- * too, exceptfor the locking.
- */
- .write = spidev_write,
- .read = spidev_read,
- .unlocked_ioctl = spidev_ioctl,
- .compat_ioctl = spidev_compat_ioctl,
- .open = spidev_open,
- .release = spidev_release,
- .llseek = no_llseek,
- };
首先看下打开函数spidev_open,程序如下:
点击(此处)折叠或打开
- static int spidev_open(struct inode*inode, struct file*filp)
- {
- struct spidev_data *spidev;
- int status = -ENXIO;
- mutex_lock(&device_list_lock);
- list_for_each_entry(spidev,&device_list, device_entry){
- if (spidev->devt== inode->i_rdev){
- status = 0;
- break;
- }
- }
- if (status== 0){
- if (!spidev->buffer){
- spidev->buffer= kmalloc(bufsiz, GFP_KERNEL);
- if (!spidev->buffer){
- dev_dbg(&spidev->spi->dev,"open/ENOMEM\n");
- status = -ENOMEM;
- }
- }
- if (status== 0){
- spidev->users++;
- filp->private_data= spidev;
- nonseekable_open(inode, filp);
- }
- } else
- pr_debug("spidev: nothing for minor %d\n", iminor(inode));
- mutex_unlock(&device_list_lock);
- return status;
- }
说明:
1) 程序首先上本地全局互斥锁。
2) 遍历链表device_list,通过设备号找到在探测函数中创建的设备结构体spidev。
3) 如果寻找成功,将其保存在file的私有成员中,供其他操作集中的函数使用。
realease函数如下:
点击(此处)折叠或打开
- static int spidev_release(struct inode*inode, struct file*filp)
- {
- struct spidev_data *spidev;
- int status = 0;
- mutex_lock(&device_list_lock);
- spidev = filp->private_data;
- filp->private_data= NULL;
- /* last close?*/
- spidev->users--;
- if (!spidev->users){
- int dofree;
- kfree(spidev->buffer);
- spidev->buffer= NULL;
- /*... after we unbound from the underlying device?*/
- spin_lock_irq(&spidev->spi_lock);
- dofree = (spidev->spi== NULL);
- spin_unlock_irq(&spidev->spi_lock);
- if (dofree)
- kfree(spidev);
- }
- mutex_unlock(&device_list_lock);
- return status;
- }
说明:
1) 首先减少使用次数。
2) 释放传输buf空间。
3) 释放结构体内存。
spi读数据函数如下:
点击(此处)折叠或打开
- static inline ssize_t
- spidev_sync_read(struct spidev_data *spidev, size_t len)
- {
- struct spi_transfer t = {
- .rx_buf = spidev->buffer,
- .len =len,
- };
- struct spi_message m;
- spi_message_init(&m);
- spi_message_add_tail(&t,&m);
- return spidev_sync(spidev,&m);
- }
- static ssize_t
- spidev_read(struct file *filp, char __user *buf, size_t count, loff_t*f_pos)
- {
- struct spidev_data *spidev;
- ssize_t status = 0;
- /* chipselect only toggles at startor end of operation*/
- if (count> bufsiz)
- return -EMSGSIZE;
- spidev = filp->private_data;
- mutex_lock(&spidev->buf_lock);
- status = spidev_sync_read(spidev, count);
- if (status> 0) {
- unsigned long missing;
- missing = copy_to_user(buf, spidev->buffer, status);
- if (missing== status)
- status = -EFAULT;
- else
- status = status - missing;
- }
- mutex_unlock(&spidev->buf_lock);
- return status;
- }
说明:
1) 进入读函数中,首先上互斥锁。
2) 然后调用spidev_sync_read函数读数据。
3) 如果读取成功,将读取到的数据拷贝到应用层。
4) 解互斥锁。
5) 在spidev_sync_read函数中,首先定义一个传输结构体spi_transfer,由于是读操作,所以只初始化其接收buf。
6) 初始化spi_message,调用函数spidev_sync传输数据,其具体程序如下:
点击(此处)折叠或打开
- static ssize_t
- spidev_sync(struct spidev_data *spidev, struct spi_message *message)
- {
- DECLARE_COMPLETION_ONSTACK(done);
- int status;
- message->complete= spidev_complete;
- message->context= &done;
- spin_lock_irq(&spidev->spi_lock);
- if (spidev->spi== NULL)
- status = -ESHUTDOWN;
- else
- status = spi_async(spidev->spi, message);
- spin_unlock_irq(&spidev->spi_lock);
- if (status== 0){
- wait_for_completion(&done);
- status = message->status;
- if (status== 0)
- status = message->actual_length;
- }
- return status;
- }
说明:
1) 首先定义并初始化一个completion。
2) 上自旋锁,调用spi_async函数传输数据,spi_async就是在Linux核心中提供的传输函数。
3) 如果调用传输函数成功,则等待。
3) wait_for_completion(&done);等待传输完成,如果传输正确,函数返回值为传输的字节数。
函数操作集写函数spidev_write程序如下:
点击(此处)折叠或打开
- static inline ssize_t
- spidev_sync_write(struct spidev_data *spidev, size_t len)
- {
- struct spi_transfer t = {
- .tx_buf = spidev->buffer,
- .len =len,
- };
- struct spi_message m;
- spi_message_init(&m);
- spi_message_add_tail(&t,&m);
- return spidev_sync(spidev,&m);
- }
- static ssize_t
- spidev_write(struct file *filp, const char __user*buf,
- size_t count, loff_t *f_pos)
- {
- struct spidev_data *spidev;
- ssize_t status = 0;
- unsigned long missing;
- /* chipselect only toggles at startor end of operation*/
- if (count> bufsiz)
- return -EMSGSIZE;
- spidev = filp->private_data;
- mutex_lock(&spidev->buf_lock);
- missing = copy_from_user(spidev->buffer, buf, count);
- if (missing== 0){
- status = spidev_sync_write(spidev, count);
- } else
- status = -EFAULT;
- mutex_unlock(&spidev->buf_lock);
- return status;
- }
说明:
1) 写函数首先上互斥锁,然后从应用层拷贝需要写的数据。
2) 如果拷贝成功,调用函数spidev_sync_write完成写数据。
3) 解互斥锁。
4) spidev_sync_write函数中,首先定义spi_transfer传输结构体,由于是写,此结构体只有tx_buf。
5) 初始化spi_message,然后调用spidev_sync传输数据,此函数在上面的读操作中已经讲述。
ioctl函数如下:
点击(此处)折叠或打开
- static long
- spidev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
- {
- int err= 0;
- int retval = 0;
- struct spidev_data *spidev;
- struct spi_device *spi;
- u32 tmp;
- unsigned n_ioc;
- struct spi_ioc_transfer *ioc;
- /* Check typeand command number */
- if (_IOC_TYPE(cmd)!= SPI_IOC_MAGIC)
- return -ENOTTY;
- /* Check access direction once here; don't repeat below.
- * IOC_DIR is from the user perspective, while access_ok is
- * from the kernel perspective; so they look reversed.
- */
- if (_IOC_DIR(cmd)& _IOC_READ)
- err =!access_ok(VERIFY_WRITE,
- (void __user *)arg, _IOC_SIZE(cmd));
- if (err== 0 && _IOC_DIR(cmd)& _IOC_WRITE)
- err =!access_ok(VERIFY_READ,
- (void __user *)arg, _IOC_SIZE(cmd));
- if (err)
- return -EFAULT;
- /* guard against device removal before,or while,
- * we issue this ioctl.
- */
- spidev = filp->private_data;
- spin_lock_irq(&spidev->spi_lock);
- spi = spi_dev_get(spidev->spi);
- spin_unlock_irq(&spidev->spi_lock);
- if (spi== NULL)
- return -ESHUTDOWN;
- /* use the buffer lock herefor triple duty:
- * - prevent I/O(from us) so calling spi_setup()is safe;
- * - prevent concurrent SPI_IOC_WR_* from morphing
- * data fields while SPI_IOC_RD_* reads them;
- * - SPI_IOC_MESSAGE needs the buffer locked"normally".
- */
- mutex_lock(&spidev->buf_lock);
- switch (cmd){
- /* read requests*/
- case SPI_IOC_RD_MODE:
- retval = __put_user(spi->mode& SPI_MODE_MASK,
- (__u8 __user *)arg);
- break;
- case SPI_IOC_RD_LSB_FIRST:
- retval = __put_user((spi->mode& SPI_LSB_FIRST)? 1 : 0,
- (__u8 __user *)arg);
- break;
- case SPI_IOC_RD_BITS_PER_WORD:
- retval = __put_user(spi->bits_per_word,(__u8 __user *)arg);
- break;
- case SPI_IOC_RD_MAX_SPEED_HZ:
- retval = __put_user(spi->max_speed_hz,(__u32 __user *)arg);
- break;
- /* write requests*/
- case SPI_IOC_WR_MODE:
- retval = __get_user(tmp,(u8 __user *)arg);
- if (retval== 0){
- u8 save = spi->mode;
- if (tmp & ~SPI_MODE_MASK){
- retval = -EINVAL;
- break;
- }
- tmp |= spi->mode& ~SPI_MODE_MASK;
- spi->mode= (u8)tmp;
- retval = spi_setup(spi);
- if (retval < 0)
- spi->mode= save;
- else
- dev_dbg(&spi->dev,"spi mode %02x\n", tmp);
- }
- break;
- case SPI_IOC_WR_LSB_FIRST:
- retval = __get_user(tmp,(__u8 __user *)arg);
- if (retval== 0){
- u8 save = spi->mode;
- if (tmp)
- spi->mode|= SPI_LSB_FIRST;
- else
- spi->mode&= ~SPI_LSB_FIRST;
- retval = spi_setup(spi);
- if (retval < 0)
- spi->mode= save;
- else
- dev_dbg(&spi->dev,"%csb first\n",
- tmp ? 'l' :'m');
- }
- break;
- case SPI_IOC_WR_BITS_PER_WORD:
- retval = __get_user(tmp,(__u8 __user *)arg);
- if (retval== 0){
- u8 save = spi->bits_per_word;
- spi->bits_per_word= tmp;
- retval = spi_setup(spi);
- if (retval < 0)
- spi->bits_per_word= save;
- else
- dev_dbg(&spi->dev,"%d bits per word\n", tmp);
- }
- break;
- case SPI_IOC_WR_MAX_SPEED_HZ:
- retval = __get_user(tmp,(__u32 __user *)arg);
- if (retval== 0){
- u32 save = spi->max_speed_hz;
- spi->max_speed_hz= tmp;
- retval = spi_setup(spi);
- if (retval < 0)
- spi->max_speed_hz= save;
- else
- dev_dbg(&spi->dev,"%d Hz (max)\n", tmp);
- }
- break;
- default:
- /* segmentedand/or full-duplex I/O request*/
- if (_IOC_NR(cmd)!= _IOC_NR(SPI_IOC_MESSAGE(0))
- || _IOC_DIR(cmd)!= _IOC_WRITE){
- retval = -ENOTTY;
- break;
- }
- tmp = _IOC_SIZE(cmd);
- if ((tmp% sizeof(struct spi_ioc_transfer))!= 0){
- retval = -EINVAL;
- break;
- }
- n_ioc = tmp / sizeof(struct spi_ioc_transfer);
- if (n_ioc== 0)
- break;
- /* copy into scratch area*/
- ioc = kmalloc(tmp, GFP_KERNEL);
- if (!ioc){
- retval = -ENOMEM;
- break;
- }
- if (__copy_from_user(ioc,(void __user *)arg, tmp)){
- kfree(ioc);
- retval = -EFAULT;
- break;
- }
- /* translateto spi_message, execute*/
- retval = spidev_message(spidev, ioc, n_ioc);
- kfree(ioc);
- break;
- }
- mutex_unlock(&spidev->buf_lock);
- spi_dev_put(spi);
- return retval;
- }
说明:
1) 函数首先判断命令是否有效,如果无效,直接退出。
2) 上互斥锁。
3) switch分支中,前面的case都是支持对SPI设备的设置,包括模式、传输位、位方向和最大速率等。
4) 在设置分支中,最后调用spi_setup实现设置,此函数最终是调用总线驱动中的gsc3280_spi_setup(struct spi_device *spi)
实现设置。
5) default分支中是进行数据传输的分支,首先判断是否是有效的数据,如果不是,退出switch分支。
6) 申请内存,复制从应用层传过来的数据。
7) 调用spidev_message函数实现数据传输。
8) 解互斥锁,退出。
spidev_message函数如下:
点击(此处)折叠或打开
- static int spidev_message(struct spidev_data*spidev,
- struct spi_ioc_transfer *u_xfers, unsigned n_xfers)
- {
- struct spi_message msg;
- struct spi_transfer *k_xfers;
- struct spi_transfer *k_tmp;
- struct spi_ioc_transfer *u_tmp;
- unsigned n, total;
- u8 *buf;
- int status = -EFAULT;
- spi_message_init(&msg);
- k_xfers = kcalloc(n_xfers, sizeof(*k_tmp), GFP_KERNEL);
- if (k_xfers== NULL)
- return -ENOMEM;
- /* Construct spi_message, copying any tx datato bounce buffer.
- * We walk the array of user-provided transfers, usingeach one
- * to initialize a kernel version of the same transfer.
- */
- buf = spidev->buffer;
- total = 0;
- for (n= n_xfers, k_tmp= k_xfers, u_tmp= u_xfers;
- n;
- n--, k_tmp++, u_tmp++){
- k_tmp->len= u_tmp->len;
- total += k_tmp->len;
- if (total> bufsiz) {
- status = -EMSGSIZE;
- goto done;
- }
- if (u_tmp->rx_buf){
- k_tmp->rx_buf= buf;
- if (!access_ok(VERIFY_WRITE,(u8 __user *)
- (uintptr_t) u_tmp->rx_buf,
- u_tmp->len))
- goto done;
- }
- if (u_tmp->tx_buf){
- k_tmp->tx_buf= buf;
- if (copy_from_user(buf,(const u8 __user*)
- (uintptr_t) u_tmp->tx_buf,
- u_tmp->len))
- goto done;
- }
- buf += k_tmp->len;
- k_tmp->cs_change= !!u_tmp->cs_change;
- k_tmp->bits_per_word= u_tmp->bits_per_word;
- k_tmp->delay_usecs= u_tmp->delay_usecs;
- k_tmp->speed_hz= u_tmp->speed_hz;
- #ifdef VERBOSE
- dev_dbg(&spidev->spi->dev,
- " xfer len %zd %s%s%s%dbits %u usec %uHz\n",
- u_tmp->len,
- u_tmp->rx_buf? "rx " : "",
- u_tmp->tx_buf? "tx " : "",
- u_tmp->cs_change? "cs " : "",
- u_tmp->bits_per_word? : spidev->spi->bits_per_word,
- u_tmp->delay_usecs,
- u_tmp->speed_hz? : spidev->spi->max_speed_hz);
- #endif
- spi_message_add_tail(k_tmp,&msg);
- }
- status = spidev_sync(spidev,&msg);
- if (status< 0)
- goto done;
- /* copy any rx data out of bounce buffer*/
- buf = spidev->buffer;
- for (n= n_xfers, u_tmp= u_xfers; n; n--, u_tmp++){
- if (u_tmp->rx_buf){
- if (__copy_to_user((u8 __user*)
- (uintptr_t) u_tmp->rx_buf, buf,
- u_tmp->len)){
- status = -EFAULT;
- goto done;
- }
- }
- buf += u_tmp->len;
- }
- status = total;
- done:
- kfree(k_xfers);
- return status;
- }
说明:
1) 申请传输的结构体内存。
2) 通过for循环,对spi_ioc_transfer类型的数据进行转换。
3) 转换中,首先对本次传输的数据大小进行累计,如果总传输量超出buf的大小,直接退出。
4) 如果本次传输是接收,则设置接收数组,并对buf进行检查,查看是否可读。
5) 如果本次传输是写,则从应用层拷贝数据。
6) 对传输中参数进行赋值,包括速度、模式、速率等等。
7) 调用spidev_sync进行数据传输。
8) 将接收数据拷贝到应用层。
三、应用层
在应用层提供了二中驱动的测试程序,在“/documenation/spi”目录下,文件名称为spidev_test.c中,具体程序如下:
点击(此处)折叠或打开
- int main(int argc, char*argv[])
- {
- int ret = 0;
- int fd;
- parse_opts(argc, argv);
- fd = open(device, O_RDWR);
- if (fd< 0)
- pabort("can't open device");
- /*
- * spi mode
- */
- ret = ioctl(fd, SPI_IOC_WR_MODE,&mode);
- if (ret== -1)
- pabort("can't set spi mode");
- ret = ioctl(fd, SPI_IOC_RD_MODE,&mode);
- if (ret== -1)
- pabort("can't get spi mode");
- /*
- * bits per word
- */
- ret = ioctl(fd, SPI_IOC_WR_BITS_PER_WORD,&bits);
- if (ret== -1)
- pabort("can't set bits per word");
- ret = ioctl(fd, SPI_IOC_RD_BITS_PER_WORD,&bits);
- if (ret== -1)
- pabort("can't get bits per word");
- /*
- * max speed hz
- */
- ret = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ,&speed);
- if (ret== -1)
- pabort("can't set max speed hz");
- ret = ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ,&speed);
- if (ret== -1)
- pabort("can't get max speed hz");
- printf("spi mode: %d\n", mode);
- printf("bits per word: %d\n", bits);
- printf("max speed: %d Hz (%d KHz)\n", speed, speed/1000);
- transfer(fd);
- close(fd);
- return ret;
- }
说明:
1) 首先打开设备。
2) 然后设置工作模式、位大小和速率等。
3) 传输数据
From:http://blog.chinaunix.net/uid-25445243-id-4059262.html
- Linux spi驱动分析(三)----spiddev分析
- Linux spi驱动分析(三)----spiddev分析
- Linux spi驱动分析(三)----spiddev分析
- Linux spi驱动分析(三)----spiddev分析
- linux spi驱动分析
- linux spi驱动分析
- linux spi驱动分析
- linux spi驱动分析
- linux spi驱动分析
- linux spi驱动分析
- linux spi驱动分析
- linux spi子系统驱动分析
- linux spi子系统驱动分析
- linux spi子系统驱动分析
- Linux下SPI驱动分析
- Linux下SPI驱动分析
- Linux下SPI驱动分析
- Linux下SPI驱动分析
- TimesTen_验证新增列后表大小变化
- Ethernet IP Core介绍
- Java创建字符串是用“”还是用构造器?
- cocos2d js 编译jsc 报错this._super is not a function
- java编程之旅第三章——变量上(变量概述和数据类型概述)
- Linux spi驱动分析(三)----spiddev分析
- poj2528Mayor's posters【线段树+离散化】
- LoadRunner主要功能
- 嵌入式底层软件开发学习系列之五调试方法
- ubuntu官方简体中文wiki,有你想要的ubuntu的一切
- <蓝牙BLE>cc2540主机读RSSI值
- 数据库事务的四大特性:ACID
- leetcode oj java Implement Stack using Queues
- kafka详解一、Kafka简介