【STM32MP135F-DK】6-Linux驱动开发--led的 platform驱动框架
<div class='showpostmsg'> 本帖最后由 qiao--- 于 2023-12-12 22:12 编辑<p>最近在看一些大佬写的程序时候发现有的地方看不懂了,于是这次回来复习一下platform驱动框架,然后基于STM32MP135F-DK这块板子利用这个驱动框架点亮led灯。</p>
<p> </p>
<p><span style="font-size:24px;"><strong>1、基础知识(platform驱动框架是什么)</strong></span></p>
<p> </p>
<p><span style="font-size:22px;"><strong>1.1驱动的分离</strong></span></p>
<p>要了解platform驱动框架是什么,首先我们得知道Linux当中驱动的分离思想。Linux为了避免代码的臃肿将主机驱动和设备驱动分开了下图可以完美的表示现在的驱动架构。</p>
<p> </p>
<p>这个就是驱动的分隔,也就是将主机驱动和设备驱动分隔开来,比如 I2C、SPI 等等都会采用驱动分隔的方式来简化驱动的开发。在实际的驱动开发中,一般 I2C 主机控制器驱动已经由半导体厂家编写好了,而设备驱动一般也由设备器件的厂家编写好了,我们只需要提供设备信息即可,比如 I2C 设备的话提供设备连接到了哪个 I2C 接口上,I2C 的速度是多少等等。相当于将设备信息从设备驱动中剥离开来,驱动使用标准方法去获取到设备信息(比如从设备树中获取到设备信息),然后根据获取到的设备信息来初始化设备。 这样就相当于驱动只负责驱动,设备只负责设备,想办法将两者进行匹配即可。这个就是 Linux 中的总线(bus)、驱动(driver)和设备(device)模型,也就是常说的驱动分离。总线就是驱动和设备信息的月老,负责给两者牵线搭桥。如下图所示:</p>
<p> </p>
<p>当我们向系统注册一个驱动的时候,总线就会在右侧的设备中查找,看看有没有与之匹配的设备,如果有的话就将两者联系起来。同样的,当向系统中注册一个设备的时候,总线就会在左侧的驱动中查找看有没有与之匹配的设备,有的话也联系起来。Linux 内核中大量的驱动程序都采用总线、驱动和设备模式。而platform驱动框架就是这一模式下的产物。</p>
<p> </p>
<p><span style="font-size:22px;"><strong>1.2platform总线</strong></span></p>
<p>前面我们讲了设备驱动的分离,并且引出了总线(bus)、驱动(driver)和设备(device)模型,比如 I2C、SPI、USB 等总线。在 SOC 中有些外设是没有总线这个概念的,但是又要使用总线、驱动和设备模型该怎么办呢?为了解决此问题,Linux 提出了 platform 这个虚拟总线,相应的就有 platform_driver 和 platform_device。</p>
<p>Linux系统内核使用bus_type结构体表示总线,此结构体定义在文件include/linux/device.h,bus_type 结构体内容如下</p>
<pre>
<code>struct bus_type {
const char *name;
const char *dev_name;
struct device *dev_root;
const struct attribute_group **bus_groups;
const struct attribute_group **dev_groups;
const struct attribute_group **drv_groups;
int (*match)(struct device *dev, struct device_driver *drv);
int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
int (*probe)(struct device *dev);
int (*remove)(struct device *dev);
void (*shutdown)(struct device *dev);
int (*online)(struct device *dev);
int (*offline)(struct device *dev);
int (*suspend)(struct device *dev, pm_message_t state);
int (*resume)(struct device *dev);
int (*num_vf)(struct device *dev);
int (*dma_configure)(struct device *dev);
const struct dev_pm_ops *pm;
const struct iommu_ops *iommu_ops;
struct subsys_private *p;
struct lock_class_key lock_key;
bool need_parent_lock;
};</code></pre>
<p>其中最重要的成员就是int (*match)(struct device *dev, struct device_driver *drv)这个函数。单词 match 的意思就是“匹配、相配”,因此此函数就是完成设备和驱动之间匹配的,总线就是使用 match 函数来根据注册的设备来查找对应的驱动,或者根据注册的驱动来查找相应的设备,因此每一条总线都必须实现此函数。match 函数有两个参数:dev 和 drv,这两个参数分别device 和 device_driver 类型,也就是设备和驱动。</p>
<p>platform 总线是 bus_type 的一个具体实例,定义在文件 drivers/base/platform.c,platform 总线定义如下:</p>
<pre>
<code>struct bus_type platform_bus_type = {
.name = "platform",
.dev_groups = platform_dev_groups,
.match = platform_match,
.uevent = platform_uevent,
.dma_configure = platform_dma_configure,
.pm = &platform_dev_pm_ops,
};</code></pre>
<p>platform_bus_type 就是 platform 平台总线,其中 platform_match 就是匹配函数。我们来看一下驱动和设备是如何匹配的,platform_match 函数定义在文件 drivers/base/platform.c 中,函数内容如下所示:</p>
<pre>
<code>static int platform_match(struct device *dev,struct device_driver *drv)
{
struct platform_device *pdev = to_platform_device(dev);
struct platform_driver *pdrv = to_platform_driver(drv);
/*When driver_override is set,only bind to the matching driver*/
if (pdev->driver_override)
return !strcmp(pdev->driver_override, drv->name);
/* Attempt an OF style match first */
if (of_driver_match_device(dev, drv))
return 1;
/* Then try ACPI style match */
if (acpi_driver_match_device(dev, drv))
return 1;
/* Then try to match against the id table */
if (pdrv->id_table)
return platform_match_id(pdrv->id_table, pdev) != NULL;
/* fall-back to driver name match */
return (strcmp(pdev->name, drv->name) == 0);
}</code></pre>
<p>驱动和设备的匹配有四种方法,我们依次来看一下:<br />
第一种匹配方式, OF 类型的匹配,也就是设备树采用的匹配方式,of_driver_match_device 函数定义在文件 include/linux/of_device.h 中。device_driver 结构体(表示设备驱动)中有个名为of_match_table的成员变量,此成员变量保存着驱动的compatible匹配表,设备树中的每个设备节点的 compatible 属性会和 of_match_table 表中的所有成员比较,查看是否有相同的条目,如果有的话就表示设备和此驱动匹配,设备和驱动匹配成功以后 probe 函数就会执行。</p>
<p>第二种匹配方式,ACPI 匹配方式。</p>
<p>第三种匹配方式,id_table 匹配,每个 platform_driver 结构体有一个 id_table成员变量,顾名思义,保存了很多 id 信息。这些 id 信息存放着这个 platformd 驱动所支持的驱动类型。</p>
<p>第四种匹配方式,如果第三种匹配方式的 id_table 不存在的话就直接比较驱动和设备的 name 字段,看看是不是相等,如果相等的话就匹配成功。<br />
对于支持设备树的 Linux 版本号,一般设备驱动为了兼容性都支持设备树和无设备树两种匹配方式。也就是第一种匹配方式一般都会存在,第三种和第四种只要存在一种就可以,一般用的最多的还是第四种,也就是直接比较驱动和设备的 name 字段,毕竟这种方式最简单了。</p>
<p> </p>
<p><span style="font-size:22px;"><strong>1.3platform驱动</strong></span></p>
<p>platform_driver 结构体表示 platform 驱动 ,此结构体定义在文件include/linux/platform_device.h 中,内容如下:</p>
<pre>
<code>struct platform_driver {
int (*probe)(struct platform_device *);
int (*remove)(struct platform_device *);
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device *, pm_message_t state);
int (*resume)(struct platform_device *);
struct device_driver driver;
const struct platform_device_id *id_table;
bool prevent_deferred_probe;
};</code></pre>
<p>probe 函数,当驱动与设备匹配成功以后 probe 函数就会执行,非常重要的函数!!一般驱动的提供者会编写,如果自己要编写一个全新的驱动,那么 probe 就需要自行实现。<br />
driver 成员,为 device_driver 结构体变量,Linux 内核里面大量使用到了面向对象的思维,device_driver 相当于基类,提供了最基础的驱动框架。plaform_driver 继承了这个基类,然后在此基础上又添加了一些特有的成员变量。<br />
id_table 表,也就是我们上一小节讲解 platform 总线匹配驱动和设备的时候采用的第三种方法,id_table 是个表(也就是数组),每个元素的类型为 platform_device_id,platform_device_id 结构体内容如下:</p>
<pre>
<code>struct platform_device_id {
char name;
kernel_ulong_t driver_data;
};</code></pre>
<p>device_driver 结构体定义在 include/linux/device.h,device_driver 结构体内容如下:</p>
<pre>
<code>struct device_driver {
const char *name;
struct bus_type *bus;
struct module *owner;
const char *mod_name; /* used for built-in modules */
bool suppress_bind_attrs; /* disables bind/unbind via sysfs */
enum probe_type probe_type;
const struct of_device_id *of_match_table;
const struct acpi_device_id *acpi_match_table;
int (*probe) (struct device *dev);
int (*remove) (struct device *dev);
void (*shutdown) (struct device *dev);
int (*suspend) (struct device *dev, pm_message_t state);
int (*resume) (struct device *dev);
const struct attribute_group **groups;
const struct attribute_group **dev_groups;
const struct dev_pm_ops *pm;
void (*coredump) (struct device *dev);
struct driver_private *p;
};</code></pre>
<p>其中of_match_table 就是采用设备树的时候驱动使用的匹配表,同样是数组,每个匹配项都为 of_device_id 结构体类型,此结构体定义在文件 include/linux/mod_devicetable.h 中,内容如下:</p>
<pre>
<code>struct of_device_id {
char name;
char type;
char compatible;
const void *data;
};</code></pre>
<p>第 4 行的 compatible 非常重要,因为对于设备树而言,就是通过设备节点的 compatible 属性值和 of_match_table 中每个项目的 compatible 成员变量进行比较,如果有相等的就表示设备和此驱动匹配成功。</p>
<p> </p>
<p><span style="font-size:22px;"><strong>1.4platform设备</strong></span></p>
<p>最新的Linux内核platform设备信息就是用设备树描述,Linux 内核启动的时候会从设备树中读取设备信息。</p>
<p> </p>
<p><span style="font-size:22px;"><strong>1.5platform驱动框架的应用</strong></span></p>
<p>在编写 platform 驱动的时候,首先定义一个 platform_driver 结构体变量,然后实现结构体中的各个成员变量,重点是实现匹配方法以及 probe 函数。当驱动和设备匹配成功以后 probe函数就会执行,具体的驱动程序在 probe 函数里面编写,比如字符设备驱动等等。</p>
<p>当我们定义并初始化好 platform_driver 结构体变量以后,需要在驱动入口函数里面调用platform_driver_register 函数向 Linux 内核注册一个 platform 驱动,platform_driver_register 函数原型如下所示:</p>
<pre>
<code>int platform_driver_register (struct platform_driver *driver)
//函数参数和返回值含义如下:
//driver:要注册的 platform 驱动。
//返回值:负数,失败;0,成功。</code></pre>
<p>还需要在驱动卸载函数中通过 platform_driver_unregister 函数卸载 platform 驱动,platform_driver_unregister 函数原型如下:</p>
<pre>
<code>void platform_driver_unregister(struct platform_driver *drv)
//函数参数和返回值含义如下:
//drv:要卸载的 platform 驱动。
//返回值:无。</code></pre>
<p>platform 驱动框架如下所示:</p>
<pre>
<code>/* 设备结构体 */
struct xxx_dev{
struct cdev cdev;
/* 设备结构体其他具体内容 */
};
struct xxx_dev xxxdev; /* 定义个设备结构体变量 */
static int xxx_open(struct inode *inode, struct file *filp)
{
/* 函数具体内容 */
return 0;
}
static ssize_t xxx_write(struct file *filp, const char __user *buf,size_t cnt, loff_t *offt)
{
/* 函数具体内容 */
return 0;
}
/*
* 字符设备驱动操作集
*/
static struct file_operations xxx_fops = {
.owner = THIS_MODULE,
.open = xxx_open,
.write = xxx_write,
};
/*
* platform 驱动的 probe 函数
* 驱动与设备匹配成功以后此函数就会执行
*/
static int xxx_probe(struct platform_device *dev)
{
......
cdev_init(&xxxdev.cdev, &xxx_fops); /* 注册字符设备驱动 */
/* 函数具体内容 */
return 0;
}
static int xxx_remove(struct platform_device *dev)
{
......
cdev_del(&xxxdev.cdev);/* 删除 cdev */
/* 函数具体内容 */
return 0;
}
/* 匹配列表 */
static const struct of_device_id xxx_of_match[] = {
{ .compatible = "xxx-gpio" },
{ /* Sentinel */ }
};
/*
* platform 平台驱动结构体*/
static struct platform_driver xxx_driver = {
.driver = {
.name = "xxx",
.of_match_table = xxx_of_match,
},
.probe = xxx_probe,
.remove = xxx_remove,
};
/* 驱动模块加载 */
static int __init xxxdriver_init(void)
{
return platform_driver_register(&xxx_driver);
}
/* 驱动模块卸载 */
static void __exit xxxdriver_exit(void)
{
platform_driver_unregister(&xxx_driver);
}
module_init(xxxdriver_init);
module_exit(xxxdriver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("AUTHOR");</code></pre>
<p><strong><span style="font-size:24px;">2、基于platform框架的led设备驱动开发</span></strong></p>
<p> </p>
<p><span style="font-size:22px;"><strong>2.1原理图查看</strong></span></p>
<p>打开STM32MP135F-DK的原理图,寻找可用的led。</p>
<p> </p>
<p>从图中的得知这个led等低电平点亮,并且在B2按钮按下的时候他也会点亮。</p>
<p> </p>
<p><span style="font-size:22px;"><strong>2.2设备树修改</strong></span></p>
<p>在stm32mp135f-dk.dts设备树文件的根文件下添加如下节点。</p>
<p> </p>
<p>编译设备树</p>
<pre>
<code>make stm32mp135f-dk.dtb LOADADDR=0xC2000040</code></pre>
<p>将设备树文件拷贝到板子的/boot目录下</p>
<pre>
<code>scp arch/arm/boot/dts/stm32mp135f-dk.dtbroot@192.168.137.28:/boot</code></pre>
<p>重启板子,在/proc/device-tree会多出一个设备树led-red的节点,就说明我们的设备树修改正确。</p>
<p> </p>
<p><span style="font-size:22px;"><strong><span id="cke_bm_11487S" style="display: none;"> </span></strong></span></p>
<p><span style="font-size:22px;"><strong>2.3platform驱动编写</strong></span></p>
<p>我们打开虚拟机,打开vscode创建一个新的Linux_mp135的文件夹,我们的代码就放在这里面。</p>
<p>因为是编写Linux 驱动,因此会用到Linux 源码中的函数。我们需要在VSCode中添加Linux源码中的头文件路径。打开 VSCode,按下“Crtl+Shift+P”打开 VSCode 的控制台,然后输“C/C++: Edit configurations(JSON) ”,打开 C/C++编辑配置文件添加源码的一些头文件路径。</p>
<p> </p>
<p>新建一个led_drive.c文件我按照驱动框架编写了如下的驱动。</p>
<pre>
<code>#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/of_gpio.h>
#include <linux/gpio.h>
#include <linux/errno.h>
#include <asm/uaccess.h>
#define DEVICE_NAME "led_red"
#define DEVICE_CNT 1
#define LED_ON 0
#define LED_OFF 1
struct led_device{
dev_t devid; //设备号
struct cdev cdev; //字符设备
struct class* class; //类
struct device* device; //设备
struct device_node* node;//led设备节点
int gpio_led; //led的gpio标号
};
/*定义设备*/
struct led_device leddev;
static void led_switch(unsigned char state){
if(state == LED_ON){
gpio_set_value(leddev.gpio_led,0);
}else if(state == LED_OFF){
gpio_set_value(leddev.gpio_led,1);
}
}
/*
* @description : 打开设备
* @param – inode : 传递给驱动的 inode
* @param - filp : 设备文件,file 结构体有个叫做 private_data 的成员变量
* 一般在 open 的时候将 private_data 指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int led_open(struct inode *inode, struct file *filp){
filp->private_data = &leddev;
return 0;
}
static int led_release(struct inode *inode, struct file *filp){
return 0;
}
/*
* @description : 向设备写数据
* @param – filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param – offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt){
int ret;
unsigned char databuf;
unsigned char ledstate;
ret = copy_from_user(databuf,buf,cnt);
if(ret < 0){
printk("kernel write failed!\r\n");
return -EFAULT;
}
ledstate = databuf;
if (ledstate == LED_ON){
led_switch(LED_ON);
}else if(ledstate == LED_OFF){
led_switch(LED_OFF);
}
return 0;
}
struct file_operations led_ops = {
.owner = THIS_MODULE,
.open = led_open,
.release = led_release,
.write = led_write
};
static int led_gpio_init(struct device_node *nd){
int ret;
/* 从设备树中获取 GPIO */
leddev.gpio_led = of_get_named_gpio(nd, "led-gpio", 0);//led-gpio在设备数中定义
if(!gpio_is_valid(leddev.gpio_led)){
printk(KERN_ERR "leddev: Failed to get led-gpio\n");
return -EINVAL;
}
/* 申请使用 GPIO */
ret = gpio_request(leddev.gpio_led,"LED0");
if(ret){
printk(KERN_ERR "led: Failed to request led-gpio\n");
return ret;
}
/* 将 GPIO 设置为输出模式并设置 GPIO 初始电平状态 */
gpio_direction_output(leddev.gpio_led,1);
return 0;
}
/*
* @description : flatform 驱动的 probe 函数,当驱动与设备匹配以后此函数
* 就会执行
* @param - dev : platform 设备
* @return : 0,成功;其他负值,失败
*/
static int led_prove(struct platform_device *pdev){
int ret;
printk("led driver and device was matched!\r\n");
/* 初始化 LED */
ret = led_gpio_init(pdev->dev.of_node); //of_node设备数节点,匹配后成功后,这里装着led的信息
if(ret){
return ret;
}
/*1.设置设备号*/
ret = alloc_chrdev_region(&leddev.devid,0,DEVICE_CNT,DEVICE_NAME);
if(ret<0){
pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n",
DEVICE_NAME, ret);
goto free_gpio;
}
/* 2、初始化 cdev */
leddev.cdev.owner = THIS_MODULE;
cdev_init(&leddev.cdev,&led_ops);
/* 3、添加一个 cdev */
ret = cdev_add(&leddev.cdev, leddev.devid, DEVICE_CNT);
if(ret<0){
goto del_unregister;
}
/* 4、创建类 */
leddev.class = class_create(THIS_MODULE, DEVICE_NAME);
if(IS_ERR(leddev.class)){
goto del_cdev;
}
/* 5、创建设备 */
leddev.device = device_create(leddev.class, NULL, leddev.devid,NULL, DEVICE_NAME);
if (IS_ERR(leddev.device)) {
goto destroy_class;
}
return 0;
destroy_class:
class_destroy(leddev.class);
del_cdev:
cdev_del(&leddev.cdev);
del_unregister:
unregister_chrdev_region(leddev.devid, DEVICE_CNT);
free_gpio:
gpio_free(leddev.gpio_led);
return -EIO;
}
/*
* @description : platform 驱动的 remove 函数
* @param - dev : platform 设备
* @return : 0,成功;其他负值,失败
*/
static int led_remove(struct platform_device *dev)
{
gpio_set_value(leddev.gpio_led, 1);/* 卸载驱动的时候关闭 LED */
gpio_free(leddev.gpio_led); /* 注销 GPIO */
cdev_del(&leddev.cdev); /* 删除 cdev */
unregister_chrdev_region(leddev.devid, DEVICE_CNT);
device_destroy(leddev.class, leddev.devid); /* 注销设备 */
class_destroy(leddev.class); /* 注销类 */
return 0;
}
/* 匹配列表 */
static const struct of_device_id led_of_match[] = {
{.compatible = "tang,led"},
{/* Sentinel */}
};
/* platform 驱动结构体 */
static struct platform_driver led_driver = {
.driver = {
.name = "stm32mp1_leds",/* 驱动名字,用于和设备匹配 */
.of_match_table = led_of_match/* 设备树匹配表 */
},
.probe =led_prove ,
.remove=led_remove
};
/**
* 驱动装配函数
* */
static int __init leddriver_init(void){
return platform_driver_register(&led_driver);
}
/**
* 驱动卸载函数
* */
static void __exit leddriver_exit(void){
platform_driver_unregister(&led_driver);
}
module_init(leddriver_init);
module_exit(leddriver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("TANG");</code></pre>
<p>新建一个Makefile文件。用于编译所写的驱动模块。</p>
<pre>
<code>KERNELDIR := /home/tang/linux/mp135dkf/Linux_source/sourceCode/stm32mp1-openstlinux-6.1-yocto-mickledore-mp1-v23.06.21/sources/arm-ostl-linux-gnueabi/linux-stm32mp-6.1.28-stm32mp-r1-r0/linux-6.1.28
CURRENT_PATH := $(shell pwd)
obj-m := led_drive.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
</code></pre>
<p>输入make进行编译,生成对应.ko文件,这个文件使我们需要拷贝到板子上的</p>
<pre>
<code>scp led_drive.koroot@192.168.137.28:/lib/modules/6.1.28 //拷贝到开发板/lib/modules/6.1.28 目录</code></pre>
<p> </p>
<p>输入下面的命令加载驱动。会提示驱动自动适配成功。</p>
<p> </p>
<p>这样我们的板子就具备led的驱动了。我们可以到/sys/class目录下和到/dev目录下看一下我们的led设备。</p>
<p> </p>
<p> </p>
<p><span style="font-size:22px;"><strong>2.4led应用程序编写</strong></span></p>
<p>新建一个led_app.c文件。编写如下的led验证程序</p>
<pre>
<code>#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#define LEDOFF 0
#define LEDON 1
/*
* @description : main主程序
* @param - argc : argv数组元素个数
* @param - argv : 具体参数
* @return : 0 成功;其他 失败
*/
int main(int argc, char *argv[])
{
int fd, retvalue;
char *filename;
unsigned char databuf;
if(argc != 3){
printf("Error Usage!\r\n");
return -1;
}
filename = argv;
/* 打开led驱动 */
fd = open(filename, O_RDWR);
if(fd < 0){
printf("file %s open failed!\r\n", argv);
return -1;
}
databuf = atoi(argv); /* 要执行的操作:打开或关闭 */
/* 向/dev/led文件写入数据 */
retvalue = write(fd, databuf, sizeof(databuf));
if(retvalue < 0){
printf("LED Control Failed!\r\n");
close(fd);
return -1;
}
retvalue = close(fd); /* 关闭文件 */
if(retvalue < 0){
printf("file %s close failed!\r\n", argv);
return -1;
}
return 0;
}</code></pre>
<p>对应用程序进行交叉编译并拷贝到板子的/home/root目录下。方便一会运行。</p>
<pre>
<code>arm-none-linux-gnueabihf-gcc led_App.c -o led_App
scp led_App root@192.168.137.28:/home/root</code></pre>
<p> </p>
<p><span style="font-size:22px;"><strong>2.5驱动验证现象</strong></span></p>
<p>当我们输入如下的命令时候就会看到现象。</p>
<p> </p>
<p>现象:</p>
<p>585944b606c183f7fe3b7edd132d21cb<br />
</p>
</div><script> var loginstr = '<div class="locked">查看本帖全部内容,请<a href="javascript:;" style="color:#e60000" class="loginf">登录</a>或者<a href="https://bbs.eeworld.com.cn/member.php?mod=register_eeworld.php&action=wechat" style="color:#e60000" target="_blank">注册</a></div>';
if(parseInt(discuz_uid)==0){
(function($){
var postHeight = getTextHeight(400);
$(".showpostmsg").html($(".showpostmsg").html());
$(".showpostmsg").after(loginstr);
$(".showpostmsg").css({height:postHeight,overflow:"hidden"});
})(jQuery);
} </script><script type="text/javascript">(function(d,c){var a=d.createElement("script"),m=d.getElementsByTagName("script"),eewurl="//counter.eeworld.com.cn/pv/count/";a.src=eewurl+c;m.parentNode.insertBefore(a,m)})(document,523)</script> <p>搞驱动的不多了。。。</p>
<p>请问下市场上现在是搞啥的多啊。我还是个学生,摸不着方向<img height="53" src="https://bbs.eeworld.com.cn/static/editor/plugins/hkemoji/sticker/facebook/sad.gif" width="54" /></p>
页:
[1]