MADL!AR
Code is cheap, show me the PPT!
首页
分类
Fragment
关于
吉利远程V7E空调改远程控制
分类:
硬件
发布于: 2026-05-10
去年9万块买了一个吉利远程V7E面包车,内部空间巨大,打算做一张床在里面去露营郊游。但是空调是个硬伤,在车子后排没法够到中控台的空调按键,而且这车的空调不能调温,开了加热就热的头痛,开了制冷就冷得哆嗦。这勾起了我的改装欲,决定外加一个模块,实现空调的远程+温控。  ### 一、总体设计 现代电动汽车有一个ECU或者VCU即整车控制器,空调按键最终连接到这里,这个控制器负责控制空调压缩机、风机和PTC等部件。所以想法是,在按键和VCU中间加一个模块,截获按键的输入,并自己伪装成按键上报到VCU。只要能完成这一步,借助WiFi模块就可以实现无线控制,并且可以在模块中集成类似PWM的调温逻辑。当然,这里受制于VCU和继电器的响应速度,肯定不能把PWM的频率设置到几khz这么高,而是一个非常低的水平,比如1分钟的周期内,加热30秒、关闭加热30秒,就能得到一个温和的温度。 ### 二、按键逆向 这辆车有两个按键面板,方向盘左下侧有一个,主要控制手刹、驾驶模式和汽车灯光等;中控台的中间有一个,用来控制空调,包括风量、温度、除霜、加热和压缩机。  拆解灯光面板发现,其中有几个接线端是12V供电和CAN_H、CAN_L引脚,一个可行的办法是逆向CAN协议,向VCU发送控制指令。但由于手头没有CAN分析仪或者开发板,而且逆向原厂协议较为复杂,暂不考虑此方案。  拆解空调面板发现,只有一些阻容按键、耳机管之类的元件,没有芯片。这样就特别适合MCU读取和模拟了。其连接器是一个20pin接口,但有6个空针,即有效数据线为14pin。而这个面板有11个按键和5个指示灯,显然这些按键要么是矩阵按键,要么是AD按键。 抄板发现电路如下:  即有以下特点: 1. (2P)有一个引脚直连电瓶的负极,另有一个引脚十分接近蓄电池电压,可以确定是12V供电引脚 2. (2P)双闪和加热按键的一端连接在这个地线上,另外一端上报到主机(未必是VCU,通过线束传到中控台某盒子)或PTC控制器 3. (6P)普通按键的背光灯占用一个引脚,双闪按键的背光灯占用一个引脚,4个指示灯占用4条线,不过这4个灯光在熄灭时为低电平,点亮时为高电平(12V)。它们和1中的12V供电形成回路 4. (4P)P14引脚与地线存在零点几V的压差,P15、P16、P17与P14的相对压差为5.045V,依次按下9个按键时,P15~P17三个引脚相对P14的电压值会轮流发生变化。可以推测9个按键分布在这四个引脚中,构成一个AD按键网络。 推测P14是VCU中从电瓶供电中隔离出来的模拟地AGND,P15~P17分别通过一个一个电阻连接到VCU的ADCin引脚上。即以P15为例: ```text 主机VCU侧: +5.045V | R_pullup | +---------> ADC输入 | P15 | [按键网络] | P14 AGND 按键PCB侧: 按键闭合时,通过电阻连接到P14(地),形成分压,主机ADC读取电压 ``` 通过分别测量不同按键按下时P15~P17的电压,可以计算得出R_pullup的值约为750欧。这个值有参考意义,在设计的模块的AD输入端电路时,可以考虑同样串接此上拉电阻,这样可以参照原按键的AD值的比例来设计按键读取的逻辑。 ### 三、电路设计 控制模块中模拟AD按键的电路:  其中,GPIO连接到TPL7407上,驱动电阻,模拟AD按键。TPL7407可以视作自带栅极下拉的NMOS管,当MCU的IO输出高电平时,对应路径上的电阻被下拉到地(P14),构成回路。 需要注意的是,不能直接将电瓶的负极短接到P14,这两路地可能是隔离的,直接短接轻则引入干扰,重则烧毁VCU。这里可以采用一个隔离的DC-DC模块解决供电的问题,从12V取电,降压到9V,再使用TPS54302降压到3.3V用作模块的电源。出于控制的方便,这里使用树莓派pico 2芯片rp2350作为主控,配合radio module 2模块实现远程通信(隔离DC-DC模块在图中未画出):  成品如下,可以完美塞入到中控台下的USB充电盒中:  ### 四、软件 由于rp2350有两个核,不借助RTOS的情况下,可以用一个核心处理网络通信,另一个核心负责按键的读取和上报。这里借助一个消息队列,将需要执行的事务放在队列中,由消费端获取执行,这样可以确保串行化逻辑,避免状态机的混乱: ``` // core0 用于处理网络,忽略 void core1_main(void) { int8_t state = 0; uint32_t msg = 0; simple_queue_init(); init_gpio(); init_adc(); aircon_init(); // 读取ADC,检测按键 init_timer_interrupt(2); while (1) { if (isr_timeout_count > 0) { while (1) { printf("error happened! overflow!\n"); sleep_ms(1000); } } // 上报按键 process_key_report(); // 读取车机状态 read_car_state(); } } ``` 其中,上报逻辑是一个简单的状态机: ``` void process_key_report() { static int8_t state = 0; // 0: IDLE, 1: PRESSED, 2: RELEASING if (state == 0) { // 空闲,从队列里取出任务执行上报 result = simple_queue_get(&msg); if (result != 0) { // 未取到数据,尝试从core0 获取 uint8_t tmp; result = recv_message(&tmp); if (result != MQ_SUCCESS) { // core0 也为空,直接返回 return; } msg = (uint32_t)tmp; } // press key uint32_t gpio = get_gpio_by_key((KeyID)msg); if (gpio == 0) { return; } gpio_put(gpio, true); // update status update_sys_state((KeyID)msg); // 需要针对消息进行状态置位 action_time = time_us_64(); state = 1; } else if (state == 1) { // 前面已经按下按键,现在等待时间释放 if ((time_us_64() - action_time) >= PRESS_TIME) { // 已经按下足够时间,现在释放 gpio_put(get_gpio_by_key((KeyID)msg), false); action_time = time_us_64(); state = 2; } } else if (state == 2) { // 已经释放,等待一定时间再重置状态 if ((time_us_64() - action_time) >= RELEASE_TIME) { state = 0; } } } ``` 按键检测的逻辑被注册到2ms的定时器中断服务中: ``` // 每2ms调用的按键检测任务 void ad_key_task_2ms(uint16_t adc_data[3]) { for (uint8_t ch = 0; ch < ADC_CH_COUNT; ch++) { // 1. 读取原始ADC值 int16_t rawValue = adc_data[ch]; // 2. 应用数字滤波 int16_t filteredValue = applyFilter(&channelCtx[ch], rawValue); channelCtx[ch].filteredValue = filteredValue; // 3. 检测按键 KeyID detectedKey = detectKeyInChannel((AdcChannel)ch, filteredValue); // 4. 更新状态机 updateKeyState((AdcChannel)ch, detectedKey); } } ``` 需要注意的是,状态机需要考虑按键在按下或抬起过程中,AD的值可能穿越其它按键的区间的问题: ``` // 按键状态定义 typedef enum { KEY_STATE_IDLE = 0, // 空闲状态 KEY_STATE_DETECTING, // 检测中 KEY_STATE_PRESSED, // 确认按下 KEY_STATE_RELEASING // 释放中 } KeyState; // 配置参数 #define IDLE_THRESHOLD 3800 // 空闲阈值 #define DEBOUNCE_COUNT 4 // 去抖次数(2ms * 3 = 6ms) #define FILTER_WINDOW_SIZE 5 // 滤波窗口大小 #define RELEASE_COUNT 3 // 释放确认次数 // 处理空闲状态 static void handleIdleState(ChannelContext* ctx, KeyID detectedKey) { if (detectedKey != KEY_NONE) { // 检测到可能的按键按下 ctx->lastKey = detectedKey; ctx->state = KEY_STATE_DETECTING; ctx->debounceCount = 1; } // 保持在空闲状态 } // 处理检测中状态 static void handleDetectingState(ChannelContext* ctx, KeyID detectedKey) { if (detectedKey == ctx->lastKey) { // 仍然是同一个按键 ctx->debounceCount++; if (ctx->debounceCount >= DEBOUNCE_COUNT) { // 去抖完成,确认按键按下 ctx->state = KEY_STATE_PRESSED; ctx->debounceCount = 0; } } else { // 按键变化,重新开始检测 if (detectedKey == KEY_NONE) { ctx->state = KEY_STATE_IDLE; } else { ctx->lastKey = detectedKey; ctx->debounceCount = 1; } } } // 处理已按下状态 static void handlePressedState(ChannelContext* ctx, KeyID detectedKey) { if (detectedKey != ctx->lastKey) { // 按键变化 if (detectedKey == KEY_NONE) { // 可能开始释放 ctx->state = KEY_STATE_RELEASING; ctx->debounceCount = 1; } } // 保持按下状态 } // 处理释放中状态 static void handleReleasingState(ChannelContext* ctx, KeyID detectedKey) { if (detectedKey == KEY_NONE) { // 仍然处于空闲状态 ctx->debounceCount++; if (ctx->debounceCount >= RELEASE_COUNT) { // 确认释放,发送按键消息 sendKeyMessage(ctx->lastKey); // 重置状态 ctx->state = KEY_STATE_IDLE; ctx->lastKey = KEY_NONE; ctx->debounceCount = 0; } } else { // 又检测到按键,可能是抖动,也可能是经历到另一个按键的区间 // 一律等待释放,归零释放计数器 // !!! 关键点 ctx->debounceCount = 1; } } static void updateKeyState(AdcChannel ch, KeyID detectedKey) { ChannelContext* ctx = &channelCtx[ch]; switch (ctx->state) { case KEY_STATE_IDLE: handleIdleState(ctx, detectedKey); break; case KEY_STATE_DETECTING: handleDetectingState(ctx, detectedKey); break; case KEY_STATE_PRESSED: handlePressedState(ctx, detectedKey); break; case KEY_STATE_RELEASING: handleReleasingState(ctx, detectedKey); break; } } ... ``` ### 五、遥控器 遥控器的消息发生逻辑,取决于上述模块的server协议,这里是每个按键对应一个操作码,发送到模块之后会返回4字节的空调状态,定义如下: ``` // 空调状态联合体 typedef union { volatile uint32_t raw; // 原始32位值 struct { uint32_t power : 1; // 电源开关 uint32_t circulation: 1; // 内外循环 uint32_t defrost : 1; // 除霜开关 uint32_t ac : 1; // AC开关 uint32_t heating : 1; // 加热开关 uint32_t fan_speed : 3; // 风速档位(0-6表示1-7档) uint32_t temperature: 7; // 温度编码(0-30对应17.0-32.0℃) (lo = 17 (temperature 0), hi = 32 (temperature 30)) uint32_t mode : 3; // 五个mode uint32_t reserved : 14; // 保留位 } bits; } AirConditioner; ``` 配合简单的GUI如下:  实测效果十分完美,把调整周期设置在30~60秒时较为平衡,既不聒噪又能保持舒适。下一步可以在遥控器中装上温湿度计,实现闭环控制。