学习中的疑惑点及解决

单片机

代码简化 —— 逻辑运算

if→找规律→简化代码

对于参数切换时部分代码:数组→索引→代码简化

P_datP_ctrolP_dat_index这三个变量协同工作,实现了一个“编辑-确认”的参数设置流程。可以将它们理解为电视遥控器上的“频道预设”功能。

问题:

一、核心比喻:电视遥控器的频道预设

想象你有一个老式电视遥控器,上面有“频道+”、“频道-”、“确认”和“切换预设”几个按钮,用来设置你最喜欢的电视频道:

  • P_ctrol数组:相当于电视当前正在播放的频道。它存储了系统当前实际生效的温度参数,P_ctrol[0]是温度上限,P_ctrol[1]是温度下限。

  • P_dat数组:相当于你在遥控器上正在编辑的频道号码。它存储了你通过按键修改但尚未确认的临时参数。

  • P_dat_index:相当于遥控器上的**“切换预设”按钮**,用来在“上限”和“下限”两个参数之间切换。当它为0时,表示你正在调整上限;当它为1时,表示你正在调整下限。

二、参数设置的完整流程

1. 进入设置模式(按下“菜单”键)

c

 1case 12://界面切换
 2    if(++seg_disp_mode == 2) seg_disp_mode = 0;
 3    if(seg_disp_mode == 0) { // 从参数设置跳转到温度显示
 4        P_dat_index = 0;
 5        if(P_dat[0] > P_dat[1]) { // 检查合理性
 6            P_ctrol[0] = P_dat[0]; // 确认上限
 7            P_ctrol[1] = P_dat[1]; // 确认下限
 8        }
 9    }
 10    if(seg_disp_mode == 1) { // 从温度显示跳转到参数设置
 11        P_dat[0] = P_ctrol[0]; // 读取当前生效的上限
 12        P_dat[1] = P_ctrol[1]; // 读取当前生效的下限
 13    }
 14break;
  • 当你按下“菜单”键(case 12),系统首先切换显示模式(seg_disp_mode)。

  • 进入设置界面时seg_disp_mode变为1):系统会先把当前正在生效的温度上下限(P_ctrol[0]P_ctrol[1])复制到编辑区P_dat[0]P_dat[1])。这就像遥控器在进入设置菜单时,先显示当前的频道号码。

  • 退出设置界面时seg_disp_mode变为0):系统会检查你设置的上限是否大于下限。如果合理,就把编辑区的值(P_dat)正式应用到系统设置中(P_ctrol),让新的参数生效。

2. 切换参数项(按下“切换预设”键)

c

 1case 13://参数切换
 2    if(seg_disp_mode == 1)//在参数设置界面
 3        P_dat_index ^= 1; // 在0和1之间切换
 4break;
  • 在设置界面中,按下“切换预设”键(case 13),会切换P_dat_index的值。

  • 这就像在遥控器上切换“频道1”和“频道2”,让你可以分别设置不同的参数。

3. 修改参数值(按下“+”或“-”键)

c

 1if(key_up == 14) { // 短按+
 2    key_flag = key_time = 0;
 3    if(++P_dat[P_dat_index] == 71)
 4        P_dat[P_dat_index] = 10;
 5}
 6if(key_up == 15) { // 短按-
 7    key_flag = key_time = 0;
 8    if(--P_dat[P_dat_index] == 9)
 9        P_dat[P_dat_index] = 71;
 10}
  • 按下“+”键(key_up == 14)或“-”键(key_up == 15)时,会根据P_dat_index的值,对P_dat数组中当前选中的参数进行增减。

  • 这就像在遥控器上按“+”或“-”来修改当前选中的频道号码。

三、为什么需要两个数组?

你可能会问,为什么不直接修改P_ctrol,而要多此一举地用P_dat来中转呢?

这正是程序设计的精妙之处!使用P_dat作为“编辑区”有三大好处:

  1. 安全检查:在将新参数应用到系统前,可以进行合理性检查(如P_dat[0] > P_dat[1])。就像遥控器在保存新频道前,会检查号码是否在有效范围内。

  2. 数据回显:进入设置界面时,将P_ctrol的值赋给P_dat,是为了在屏幕上显示出当前的参数值,让你知道从哪里开始修改。

  3. 事务性操作:参数的修改是“原子”的,即要么全部生效,要么全部不生效,避免了在修改过程中系统使用到不完整或错误的参数。

四、生动的遥控器操作示例

  1. 初始状态:电视正在播放频道5(相当于P_ctrol[0] = 30, P_ctrol[1] = 20)。

  2. 进入设置:你按下“菜单”键,遥控器显示“频道预设1:5”(相当于P_dat[0] = P_ctrol[0] = 30)。

  3. 修改参数:你按“+”键,频道号变成6(相当于P_dat[0] = 31);再按“切换预设”,准备设置频道2。

  4. 确认修改:你再次按下“菜单”键,遥控器将预设频道1从5更新为6(相当于P_ctrol[0] = P_dat[0]),电视现在开始播放频道6。

五、关键Tips

  1. 数组索引P_dat_index作为索引,让程序能用统一的代码处理不同的参数,避免了为上限和下限分别编写一套几乎一样的代码,使程序更简洁、更易于维护。

  2. 边界检查:代码中的if(++P_dat[P_dat_index] == 71)if(--P_dat[P_dat_index] == 9)是边界检查,防止参数值超出有效范围(10-70),确保了程序的健壮性。

  3. 数据一致性:只有在参数设置界面(seg_disp_mode == 1)才会响应“+”、“-”和“切换预设”键,而在温度显示界面则不会,这保证了数据修改的上下文一致性。

通过这种“编辑-确认”的模式,程序实现了对系统参数的安全、可靠修改,这在嵌入式系统开发中是一种非常常见且有效的设计模式。

这两行代码看起来像是“把A给B”和“把B给A”,确实非常绕。但在嵌入式开发中,这是一种非常高明且标准的**“双缓冲”**设计模式。

为了让你彻底明白,我们把这两个数组想象成两个不同的人,他们手里各有一张纸。

:people_holding_hands: 角色设定

  • P_ctrol (控制者):这是老板。他手里拿着的纸(数值)是最终决定,直接决定了机器怎么运行(比如加热还是制冷)。老板的决定一旦做出,机器就立刻执行。

  • P_dat (数据者):这是秘书。他手里拿着的纸是草稿,是用来给老板修改用的。秘书在草稿纸上随便涂改,老板和机器是完全不知道的,机器也不会受影响。

  • P_dat_index:这是秘书手中的,指着草稿纸上的哪一个格子(上限还是下限)正在被修改。


:counterclockwise_arrows_button: 逻辑详解

1. 进入设置界面时:秘书去抄老板的作业

代码逻辑:

c

 1// 秘书把老板现在的数值抄过来
 2P_dat[0] = P_ctrol[0]; 
 3P_dat[1] = P_ctrol[1]; 

场景模拟:
当你按下“设置”按钮进入界面时,你想修改参数。

  • 问题:秘书的草稿纸上本来可能是乱码,或者是上次没改完的废稿。

  • 动作:为了让秘书知道现在的参数是多少,他必须先看一眼老板手里的纸,把老板现在的数值(比如上限30,下限20)抄到自己的草稿纸上。

  • 结果:现在秘书和老板的纸一模一样。这是修改的起点。

2. 调节按键时:秘书在草稿纸上涂改

代码逻辑:
你在按键时,代码修改的是 P_dat[P_dat_index](秘书的草稿纸)。
场景模拟:
你在键盘上按“+”号,想把温度上限调高。

  • 动作:秘书在自己的草稿纸上把“30”划掉,改成“35”。(此时老板完全没动,还在看他的“30”)。

  • 好处:如果你按住不放,数字会一直变(31, 32, 33…)。如果这时候机器就直接执行,机器会疯掉,一会儿加热一会儿停。所以,只改秘书的草稿,不打扰老板

3. 退出设置界面时:秘书把草稿交给老板(关键!)

代码逻辑:

c

 1// 老板采纳秘书的草稿
 2P_ctrol[0] = P_dat[0]; 
 3P_ctrol[1] = P_dat[1]; 

场景模拟:
你调好数字(比如上限35,下限25)了,按下“退出”或“确认”。

  • 动作:这时候,秘书拿着改好的草稿纸跑到老板面前:“老板,按这个执行!” 老板这才把秘书手里的新数值抄到自己的纸上。

  • 结果此时此刻,机器才真正接收到新的指令(35度),开始改变行为。


:exploding_head: 为什么不能直接改老板的纸(P_ctrol)?

如果你不用 P_dat,直接在按键时修改 P_ctrol,会发生以下可怕的事情:

  1. 机器抽搐:你按一下“+”,机器觉得上限变成31,开始调整;你再按一下变成32,机器又调整……在你确认之前,机器就已经开始乱动了。

  2. 逻辑冲突:假设你在设置上限,还没设置下限。如果你直接改了 P_ctrol[0],而 P_ctrol[1] 还是旧的,这时候可能会出现“上限比下限还低”的逻辑错误,机器直接报错或短路。

  3. 没得后悔:一旦按错,机器立刻执行。而用“双缓冲”,你按错了没关系,只要不退出界面,老板就不知道,机器也不会动。

:pushpin: 总结

  • P_dat = P_ctrol“初始化”。进入设置时,让草稿纸显示当前值。

  • P_ctrol = P_dat“生效”。退出设置时,让修改后的值真正执行。

这就像是**“排练”(改P_dat)和“演出”**(P_ctrol控制机器)。排练的时候怎么出错都没事,只有等导演喊“通过”(退出界面)的那一刻,排练的内容才变成正式的演出。

PCF8951遇到问题

1. 最大的坑:PCF8591 的读写逻辑缺失

文档要求根据按键 S5 来决定 DAC 输出是“跟随电位器”还是“固定 2.0V”。

  • 你的代码现状: 你只读取了 dat = Ad_Read(0x43);(读电位器),然后直接把读到的值赋给了 dat2。你完全没有调用 Da_Write(写 DAC)的函数。

  • 后果: 无论你怎么按 S5,PCF8591 的 DAC 都不会输出电压,或者一直保持上一次的状态。

2. 第二个坑:LED 指示灯逻辑没写

文档对 L1-L4 的灯有非常明确的要求:

  • L1/L2:根据显示界面(U 或 F)切换。

  • L3:根据电位器电压范围闪烁(1.5V-2.5V亮,2.5V-3.5V灭…)。

  • L4:根据 S5 模式(跟随模式亮,固定模式灭)。

  • 你的代码现状: Led_Proc() 函数里是空的。

  • 后果: 灯根本不亮,或者不按规则亮。

3. 第三个坑:变量定义混乱

  • 你的代码现状: 你定义了 unsigned char dat, dat2;,然后在 Seg_Proc 里又定义了一次 dat, dat2

  • 后果: 这会导致编译错误(重复定义),或者逻辑混乱。

4. 第四个坑:数码管显示逻辑错误

  • 你的代码现状:Seg_Proc 里,你用 dat/100%10 这种方式处理电压。这是把电压当成“整数”处理了。

  • 后果: 假设 ADC 读到的值是 128(代表 2.5V 左右),你这样除会直接变成 1、2、8,显示出来是乱码,而不是 2.50

  • 正确做法: 需要将 ADC 的 0-255 映射到 0-5V,然后通过乘除法把小数点提出来。