安卓11-HDMI插拔检测流程

hdmi从插入到拔出经过底层一系列检测到应用层,应用层获取hdmi插入状态后又会做出一系列相应的动作,下面梳理了从应用层到底层一步步追踪到芯片的hpd-pin的检测过程。其大致原理就是framework层通过检测/sys/class/extcon/hdmi/state 来获取hdmi插入与否,具体更新这个状态的地方再kernel层,kernel层通过一个dw_hdmi_connector_detect轮询函数不断的查询hpd状态然后更新sys/class/extcon/hdmi/state,后面分析了hdmi插入拔出状态改变后系统所做的其它动作。


frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java +775


    private class HdmiVideoExtconUEventObserver extends ExtconStateObserver<Boolean> {
        private static final String HDMI_EXIST = "HDMI=1";
        private static final String NAME = "hdmi";
        private final ExtconInfo mHdmi = new ExtconInfo(NAME);  //判断/sys/class/extcon/hdmi文件是否存在

        private boolean init() {
            boolean plugged = false;
            try {
                plugged = parseStateFromFile(mHdmi); //解析sys/class/extcon/hdmi/state这个文件内容
            } catch (FileNotFoundException e) {
                Slog.w(TAG, mHdmi.getStatePath()
                        + " not found while attempting to determine initial state", e);
            } catch (IOException e) {
                Slog.e(
                        TAG,
                        "Error reading " + mHdmi.getStatePath()
                                + " while attempting to determine initial state",
                        e);
            }
            startObserving(mHdmi);  //开始监控/sys/class/extcon/hdmi
            return plugged;
        }

        @Override
        public void updateState(ExtconInfo extconInfo, String eventName, Boolean state) {//通过这里更新hdmi plug状态,这个转态来自hal层
                 //这里的变化来自kernel层dw-hdmi.c
						mDefaultDisplayPolicy.setHdmiPlugged(state);
        }

        @Override
        public Boolean parseState(ExtconInfo extconIfno, String state) {
            // extcon event state changes from kernel4.9
            // new state will be like STATE=HDMI=1
            return state.contains(HDMI_EXIST);   //判断这里是否sys/class/extcon/hdmi/state  HDMI=1 是的话返回true
        }
    }
     void initializeHdmiStateInternal() {
        boolean plugged = false;
        // watch for HDMI plug messages if the hdmi switch exists
        if (new File("/sys/devices/virtual/switch/hdmi/state").exists()) {
            mHDMIObserver.startObserving("DEVPATH=/devices/virtual/switch/hdmi");

            final String filename = "/sys/class/switch/hdmi/state";
            FileReader reader = null;
            try {
                reader = new FileReader(filename);
                char[] buf = new char[15];
                int n = reader.read(buf);
                if (n > 1) {
                    plugged = 0 != Integer.parseInt(new String(buf, 0, n - 1));
                }
            } catch (IOException ex) {
                Slog.w(TAG, "Couldn't read hdmi state from " + filename + ": " + ex);
            } catch (NumberFormatException ex) {
                Slog.w(TAG, "Couldn't read hdmi state from " + filename + ": " + ex);
            } finally {
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (IOException ex) {
                    }
                }
            }
        } else if (ExtconUEventObserver.extconExists()   //走这里  判断sys/class/extcon是否存在
                && ExtconUEventObserver.namedExtconDirExists(HdmiVideoExtconUEventObserver.NAME)) {
            Log.i("fan","xtconUEventObserver.extconExists");
            HdmiVideoExtconUEventObserver observer = new HdmiVideoExtconUEventObserver();//新建一个hdmi观察者,检测hdmi hpd引脚的变化
            plugged = observer.init();
            mHDMIObserver = observer;
        } else if (localLOGV) {
            Slog.v(TAG, "Not observing HDMI plug state because HDMI was not found.");
        }

        // This dance forces the code in setHdmiPlugged to run.
        // Always do this so the sticky intent is stuck (to false) if there is no hdmi.
        mDefaultDisplayPolicy.setHdmiPlugged(plugged, true /* force */);
    }   
 frameworks/base/services/core/java/com/android/server/ExtconStateObserver.java   
    public void onUEvent(ExtconInfo extconInfo, UEvent event) {
        if (LOG) Slog.d(TAG, extconInfo.getName() + " UEVENT: " + event);
        String name = event.get("NAME");
        S state = parseState(extconInfo, event.get("STATE"));
        Slog.d("fan","onUEvent get name="+name+"state="+state);
        if (state != null) {
            updateState(extconInfo, name, state);
        }
    }

public abstract S parseState(ExtconInfo extconInfo, String state);  //在PhoneWindowManager.java里实现判断sys/class/extcon/hdmi/state  是否与HDMI=1相等

frameworks/base/services/core/java/com/android/server/wm/DisplayPolicy.java    
	String ACTION_HDMI_PLUGGED = "android.intent.action.HDMI_PLUGGED";

    public void setHdmiPlugged(boolean plugged) {
        setHdmiPlugged(plugged, false /* force */);
    }

    public void setHdmiPlugged(boolean plugged, boolean force) {
        if (force || mHdmiPlugged != plugged) {
            mHdmiPlugged = plugged;
            mService.updateRotation(true /* alwaysSendConfiguration */, true /* forceRelayout */);
            final Intent intent = new Intent(ACTION_HDMI_PLUGGED);
            intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
            intent.putExtra(EXTRA_HDMI_PLUGGED_STATE, plugged);
            mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);//通知系统hdmi插入状态
        }
    }

    boolean isHdmiPlugged() {
        return mHdmiPlugged;
    }    
frameworks/base/services/core/java/com/android/server/ExtconUEventObserver.java    
     public static boolean extconExists() {
        File extconDir = new File("/sys/class/extcon"); //检查这个文件是否存在,对应上面的else if (ExtconUEventObserver.extconExists() 
        return extconDir.exists() && extconDir.isDirectory();
    }

kernel/drivers/gpu/drm/bridge/synopsys/dw-hdmi.c
static const struct drm_connector_funcs dw_hdmi_connector_funcs = {
        .fill_modes = drm_helper_probe_single_connector_modes,
        .detect = dw_hdmi_connector_detect, //通过这里检测hdmi变化
        .destroy = drm_connector_cleanup,
        .force = dw_hdmi_connector_force,
        .reset = drm_atomic_helper_connector_reset,
        .set_property = dw_hdmi_connector_set_property,
        .atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state,
        .atomic_destroy_state = drm_atomic_helper_connector_destroy_state,
        .atomic_set_property = dw_hdmi_atomic_connector_set_property,
        .atomic_get_property = dw_hdmi_atomic_connector_get_property,
};

static enum drm_connector_status
dw_hdmi_connector_detect(struct drm_connector *connector, bool force)
{
        struct dw_hdmi *hdmi = container_of(connector, struct dw_hdmi,
                                             connector);
        enum drm_connector_status result;

        if (!hdmi->force_logo) {
                mutex_lock(&hdmi->mutex);
                hdmi->force = DRM_FORCE_UNSPECIFIED;
                dw_hdmi_update_power(hdmi);
                dw_hdmi_update_phy_mask(hdmi);
                mutex_unlock(&hdmi->mutex);
        }

        result = hdmi->phy.ops->read_hpd(hdmi, hdmi->phy.data);
        if (result == connector_status_connected)
                extcon_set_state_sync(hdmi->extcon, EXTCON_DISP_HDMI, true);
        else
                extcon_set_state_sync(hdmi->extcon, EXTCON_DISP_HDMI, false);

.....
kernel/drivers/extcon/extcon.c
int extcon_set_state_sync(struct extcon_dev *edev, unsigned int id, bool state)
{
        int ret, index;
        unsigned long flags;

        index = find_cable_index_by_id(edev, id);
        if (index < 0)
                return index;

        /* Check whether the external connector's state is changed. */
        spin_lock_irqsave(&edev->lock, flags);
        ret = is_extcon_changed(edev, index, state);
        spin_unlock_irqrestore(&edev->lock, flags);
        if (!ret)
                return 0;

        ret = extcon_set_state(edev, id, state);
        if (ret < 0)
                return ret;

        return extcon_sync(edev, id);  //hdmi接入拔出状态最终在这里改变 即sys/class/extcon/hdmi/state  HDMI=1或者HDMI=0
}
//上面系统检测sys/class/extcon/hdmi/state的分析已经结束了,下面是hdmi状态变化通知系统做出改变设备物理地址的分析

\hardware\rockchip\hdmicec\hdmicec_event.cpp
static void *uevent_loop(void *param)
{
	hdmi_cec_context_t * ctx = reinterpret_cast<hdmi_cec_context_t *>(param);
	char thread_name[64] = HDMI_CEC_UEVENT_THREAD_NAME;
	hdmi_event_t cec_event;
	struct pollfd pfd[2];
	int fd[2];
	int ret, i;

	prctl(PR_SET_NAME, (unsigned long) &thread_name, 0, 0, 0);
	setpriority(PRIO_PROCESS, 0, HAL_PRIORITY_URGENT_DISPLAY);

	fd[0] = ctx->fd;
	if (fd[0] < 0) {
		ALOGE ("%s:not able to open cec state node", __func__);
		return NULL;
	}

	pfd[0].fd = fd[0];
	if (pfd[0].fd >= 0)
		pfd[0].events = POLLIN | POLLRDNORM | POLLPRI;

	while (true) {
                usleep(1000);
		int err = poll(&pfd[0], 1, 20);

		if (!err) {
			continue;
		} else if(err > 0) {
			if (!ctx->enable || !ctx->system_control)
				continue;
			ALOGD("poll revent:%02x\n", pfd[0].revents);
			memset(&cec_event, 0, sizeof(hdmi_event_t));
			if (pfd[0].revents & (POLLIN)) {
				struct cec_msg cecframe;

				ALOGD("poll receive msg\n");
				ret = ioctl(pfd[0].fd, CEC_RECEIVE, &cecframe);
				if (!ret) {
					cec_event.type = HDMI_EVENT_CEC_MESSAGE;
					cec_event.dev = &ctx->device;
					cec_event.cec.initiator = (cec_logical_address_t)(cecframe.msg[0] >> 4);
					cec_event.cec.destination = (cec_logical_address_t)(cecframe.msg[0] & 0x0f);
					cec_event.cec.length = cecframe.len - 1;
					cec_event.cec.body[0] = cecframe.msg[1];
					if (!validcecmessage(cec_event)) {
						for (ret = 0; ret < cec_event.cec.length; ret++)
						     cec_event.cec.body [ret + 1] = cecframe.msg[ret + 2];
						for (i = 0; i < cecframe.len; i++)
							ALOGD("poll receive msg[%d]:%02x\n", i, cecframe.msg[i]);
						if (ctx->event_callback)
							ctx->event_callback(&cec_event, ctx->cec_arg);
					} else {
						ALOGE("%s cec_event length > 15 ", __func__);
					}
				} else {
					ALOGE("%s hdmi cec read error", __FUNCTION__);
				}
			}

			if (pfd[0].revents & (POLLPRI)) {
				int state = -1;
				struct cec_event event;

				ALOGI("poll receive event\n");
				ret = ioctl(pfd[0].fd, CEC_DQEVENT, &event);//取得一个cec事件,然后判断事件的状态,此部分内容在内核层
				if (!ret) {
					ALOGD("event:%d\n", event.event);
					if (event.event == CEC_EVENT_PIN_HPD_LOW) {//获取底层hpdin管教状态
						ALOGI("CEC_EVENT_PIN_HPD_LOW\n");
						ctx->hotplug = false;
						cec_event.type = HDMI_EVENT_HOT_PLUG;
						cec_event.dev = &ctx->device;
						cec_event.hotplug.connected = HDMI_NOT_CONNECTED;
						cec_event.hotplug.port_id = HDMI_CEC_PORT_ID;
						if (ctx->event_callback)
							ctx->event_callback(&cec_event, ctx->cec_arg);
					} else if (event.event == CEC_EVENT_PIN_HPD_HIGH) {//高为连接
						ALOGI("CEC_EVENT_PIN_HPD_HIGH\n");
						ctx->hotplug = true;
						cec_event.type = HDMI_EVENT_HOT_PLUG;
						cec_event.dev = &ctx->device;
						cec_event.hotplug.connected = HDMI_CONNECTED;
						cec_event.hotplug.port_id = HDMI_CEC_PORT_ID;
						if (ctx->event_callback)
							ctx->event_callback(&cec_event, ctx->cec_arg);
					} else if (event.event == CEC_EVENT_STATE_CHANGE) {
						ALOGD("adapt state change,phy_addr:%x,flags:%x\n", event.state_change.phys_addr, event.flags);

						/*
						 * Before cec HAL is initialized, hdmi hpd state may be
						 * changed. So we should confirm the hpd status
						 * after cec is initialized(Kernel will report
						 * CEC_EVENT_FL_INITIAL_STATE to notify HAL that
						 * initialization is done).
						 */
						if (event.flags & CEC_EVENT_FL_INITIAL_STATE) {
							ALOGD("cec adapter init complete, get connect state\n");
							ctx->hotplug = get_hpd_state_from_node(ctx);
							ctx->cec_init = true;

							/*
							 * Framework will start la polling when box turn on,
							 * In addition, as soon as framewrok receives hdmi
							 * plug in, it will start la polling immediately.
							 * There is not need to report plug in event if hdmi
							 * is connecting when box turn on. So we should report
							 * hdmi plug out only.
							 */
							if (!ctx->hotplug)
								report_hdp_event(ctx, ctx->hotplug);
						}
						ctx->phy_addr = event.state_change.phys_addr;
					}
				} else {
					ALOGE("%s cec event get err, ret:%d\n", __func__, ret);
				}
			}
		} else {
			ALOGE("%s: cec poll failed errno: %s", __FUNCTION__,
			strerror(errno));
			continue;
		}
	}
	return NULL;
}
Kernel/drivers/media/cec/cec-api.c 
static long cec_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
 case CEC_DQEVENT:
                return cec_dqevent(adap, fh, block, parg);  //hal层调用这里获取一个cec事件,既然有获取事件就有把事件放入事件队列的地方
 drivers/media/cec/cec-adap.c
                
void cec_queue_pin_hpd_event(struct cec_adapter *adap, bool is_high, ktime_t ts)
{
        struct cec_event ev = {
                .event = is_high ? CEC_EVENT_PIN_HPD_HIGH :
                                   CEC_EVENT_PIN_HPD_LOW,
        };
        struct cec_fh *fh;

        if (!adap)
                return;
        /* hdmi HPD may occur before devnode is registered */
        if (!adap->devnode.registered)
                return;
        mutex_lock(&adap->devnode.lock);
        list_for_each_entry(fh, &adap->devnode.fhs, list)
                cec_queue_event_fh(fh, &ev, ktime_to_ns(ts)); //插入一个cec事件,把这个事件放入到ece事件队列,供hal层获取,hal层获取后传到framework层
        mutex_unlock(&adap->devnode.lock);
}

hardware/rockchip/hdmicec/hdmicec_event.cpp
static void report_hdp_event(hdmi_cec_context_t* ctx, bool hpd)
{
        hdmi_event_t cec_event;

        cec_event.type = HDMI_EVENT_HOT_PLUG;
        cec_event.dev = &ctx->device;
        if (hpd)
                cec_event.hotplug.connected = HDMI_CONNECTED;
        else
                cec_event.hotplug.connected = HDMI_NOT_CONNECTED;
        cec_event.hotplug.port_id = HDMI_CEC_PORT_ID;
        if (ctx->event_callback)
                ctx->event_callback(&cec_event, ctx->cec_arg);
}
最终通过event_callback通知应用层,下面查找下callback
hardware/rockchip/hdmicec/hdmi_cec.cpp
static void hdmi_cec_register_event_callback(const struct hdmi_cec_device* dev,
                                             event_callback_t callback, void* arg)
{
        struct hdmi_cec_context_t* ctx = (struct hdmi_cec_context_t*)dev;

        ALOGI("%s", __func__);
        ctx->event_callback = callback;
        ctx->cec_arg = arg;
}	
static int hdmi_cec_device_open(const struct hw_module_t* module, const char* name,
                                struct hw_device_t** device)
{  
	....
	  dev->device.register_event_callback = hdmi_cec_register_event_callback; //在HdmiCec.cpp完成注册
	  ......
hardware/interfaces/tv/cec/1.0/default/HdmiCec.cpp 
Return<void> HdmiCec::setCallback(const sp<IHdmiCecCallback>& callback) {  //接口在IHdmiCecCallback.hal中
    if (mCallback != nullptr) {
        mCallback->unlinkToDeath(this);
        mCallback = nullptr;
    }

    if (callback != nullptr) {
        mCallback = callback;
        mCallback->linkToDeath(this, 0 /*cookie*/);
        mDevice->register_event_callback(mDevice, eventCallback, nullptr);  //注册回调通知framework层 eventCallback看hardware/interfaces/tv/cec/1.0/default/HdmiCec.h
    }
    return Void();
}
IHdmiCec* HIDL_FETCH_IHdmiCec(const char* hal) {
    hdmi_cec_device_t* hdmi_cec_device;
    int ret = 0;
    const hw_module_t* hw_module = nullptr;

    ret = hw_get_module (HDMI_CEC_HARDWARE_MODULE_ID, &hw_module);
    if (ret == 0) {
        ret = hdmi_cec_open (hw_module, &hdmi_cec_device);
        if (ret != 0) {
            LOG(ERROR) << "hdmi_cec_open " << hal << " failed: " << ret;
        }
    } else {
        LOG(ERROR) << "hw_get_module " << hal << " failed: " << ret;
    }

    if (ret == 0) {
        return new HdmiCec(hdmi_cec_device);
    } else {
        LOG(ERROR) << "Passthrough failed to load legacy HAL.";
        return nullptr;
    }
}
hardware/interfaces/tv/cec/1.0/IHdmiCecCallback.hal        
interface IHdmiCecCallback {
    /**
     * The callback function that must be called by HAL implementation to notify
     * the system of new CEC message arrival.
     */
    oneway onCecMessage(CecMessage message);

    /**
     * The callback function that must be called by HAL implementation to notify
     * the system of new hotplug event.
     */
    oneway onHotplugEvent(HotplugEvent event);
};
hardware/interfaces/tv/cec/1.0/default/HdmiCec.h
static void eventCallback(const hdmi_event_t* event, void* /* arg */) {
        if (mCallback != nullptr && event != nullptr) {
            if (event->type == HDMI_EVENT_CEC_MESSAGE) {
                size_t length = std::min(event->cec.length,
                        static_cast<size_t>(MaxLength::MESSAGE_BODY));
                CecMessage cecMessage {
                    .initiator = static_cast<CecLogicalAddress>(event->cec.initiator),
                    .destination = static_cast<CecLogicalAddress>(event->cec.destination),
                };
                cecMessage.body.resize(length);
                for (size_t i = 0; i < length; ++i) {
                    cecMessage.body[i] = static_cast<uint8_t>(event->cec.body[i]);
                }
                mCallback->onCecMessage(cecMessage);  //实现在HdmiCecController.java 看下面
            } else if (event->type == HDMI_EVENT_HOT_PLUG) {
                HotplugEvent hotplugEvent {
                    .connected = event->hotplug.connected > 0,
                    .portId = static_cast<uint32_t>(event->hotplug.port_id)
                };
                mCallback->onHotplugEvent(hotplugEvent);  //实现在HdmiCecController.java 看下面
            }
        }  

frameworks/base/services/core/java/com/android/server/hdmi/HdmiCecController.java 
import android.hardware.tv.cec.V1_0.IHdmiCecCallback;
private IHdmiCec mHdmiCec;
    final class HdmiCecCallback extends IHdmiCecCallback.Stub { //接口在hal层IHdmiCecCallback.hal
        @Override
        public void onCecMessage(CecMessage message) throws RemoteException {
            byte[] body = new byte[message.body.size()];
            for (int i = 0; i < message.body.size(); i++) {
                body[i] = message.body.get(i);
            }
            runOnServiceThread(
                    () -> handleIncomingCecCommand(message.initiator, message.destination, body));
        }

        @Override
        public void onHotplugEvent(HotplugEvent event) throws RemoteException {
            runOnServiceThread(() -> handleHotplug(event.portId, event.connected));
        }
    }
    private final HdmiControlService mService;
    @ServiceThreadOnly
    private void handleHotplug(int port, boolean connected) {
        assertRunOnServiceThread();
        HdmiLogger.debug("Hotplug event:[port:%d, connected:%b]", port, connected);
        addHotplugEventToHistory(port, connected);
        mService.onHotplug(port, connected);
    }
    
private void init(NativeWrapper nativeWrapper) {
        mIoHandler = new Handler(mService.getIoLooper());
        mControlHandler = new Handler(mService.getServiceLooper());
        nativeWrapper.setCallback(new HdmiCecCallback());
    }

   public void setCallback(HdmiCecCallback callback) {  //应用层调用
            try {
                mHdmiCec.setCallback(callback);
            } catch (RemoteException e) {
                HdmiLogger.error("Couldn't initialise tv.cec callback : ", e);
            }
        }
frameworks/base/services/core/java/com/android/server/hdmi/HdmiControlService.java

 void onHotplug(int portId, boolean connected) {
        assertRunOnServiceThread();

        if (connected && !isTvDevice()
                && getPortInfo(portId).getType() == HdmiPortInfo.PORT_OUTPUT) {
            if (isSwitchDevice()) {
                initPortInfo();
                HdmiLogger.debug("initPortInfo for switch device when onHotplug from tx.");
            }
            ArrayList<HdmiCecLocalDevice> localDevices = new ArrayList<>();
            for (int type : mLocalDevices) {
                if (type == HdmiDeviceInfo.DEVICE_PLAYBACK
                        && isHdmiCecNeverClaimPlaybackLogicAddr) {
                    continue;
                }
                HdmiCecLocalDevice localDevice = mCecController.getLocalDevice(type);
                if (localDevice == null) {
                    localDevice = HdmiCecLocalDevice.create(this, type);
                    localDevice.init();
                }
                localDevices.add(localDevice);
            }
            allocateLogicalAddress(localDevices, INITIATED_BY_HOTPLUG);
        }

        for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) {
            device.onHotplug(portId, connected);
        }
        announceHotplugEvent(portId, connected);
    }
    private void announceHotplugEvent(int portId, boolean connected) {
        HdmiHotplugEvent event = new HdmiHotplugEvent(portId, connected);
        synchronized (mLock) {
            for (HotplugEventListenerRecord record : mHotplugEventListenerRecords) {
                invokeHotplugEventListenerLocked(record.mListener, event);
            }
        }
    }
    private void invokeHotplugEventListenerLocked(IHdmiHotplugEventListener listener,
            HdmiHotplugEvent event) {
        try {
            listener.onReceived(event);
        } catch (RemoteException e) {
            Slog.e(TAG, "Failed to report hotplug event:" + event.toString(), e);
        }
    }
frameworks/base/core/java/android/hardware/hdmi/HdmiControlManager.java
    private IHdmiHotplugEventListener getHotplugEventListenerWrapper(
            final HotplugEventListener listener) {
        return new IHdmiHotplugEventListener.Stub() {
            @Override
            public void onReceived(HdmiHotplugEvent event) {
                listener.onReceived(event);;
            }
        };
    }

    private final class ClientHotplugEventListener implements HotplugEventListener {

        @Override
        public void onReceived(HdmiHotplugEvent event) {
            List<HdmiPortInfo> ports = new ArrayList<>();
            try {
                ports = mService.getPortInfo();
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
            if (ports.isEmpty()) {
                Log.e(TAG, "Can't find port info, not updating connected status. "
                        + "Hotplug event:" + event);
                return;
            }
            // If the HDMI OUT port is plugged or unplugged, update the mLocalPhysicalAddress
            for (HdmiPortInfo port : ports) {
                if (port.getId() == event.getPort()) {
                    if (port.getType() == HdmiPortInfo.PORT_OUTPUT) {
                        setLocalPhysicalAddress(
                                event.isConnected()
                                ? port.getAddress()
                                : INVALID_PHYSICAL_ADDRESS);
                    }
                    break;
                }
            }
        }
    }        
        

	  

	
	

	

  • 9
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

技术求索者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值