自己动手写个小应用

之前尝试用wxWidgets框架写应用,后来越写越不顺手,所以当代码到达3000行的时候,我就动摇了。想了2-3天,不断地摇摆,最终决定放弃使用wxWidgets,自己实现。而且不借助已有的GUI框架,自己实现用到的UI组件。

因为我想实现的应用程序本身并不复杂,用到的UI组件也不会很多,所以实现难度估计应该不大。实际上经过这几天的初步尝试,感觉也还是非常良好的。

自己实现组件,主要就是对渲染和交互的控制,而且我只需要不到10个基础组件,所以现在代码量2000行左右,已经有了一个大概的模样,预计再有一周左右就可以粗糙的实现出来了。

自己动手实现这些组件更大的好处是控制权限更高,例如事件的传递机制,我可以按照自己的想法进行链式的、广播式的、或者就是定向的传递,十分的灵活。当然这种灵活的前提是建立在良好的封装的基础上的,否则一旦没有及时的进行归纳和封装,就有可能将代码写乱、写花,所以每一个新机能的增加,都要对所有已实现的部分进行一边重构、归纳,这是比较花时间的事情。

不过这样做也有好处,就是一旦当前的小工具实现完毕,就拥有了一个简陋的Simple GUI,这样可以为今后再实现其他软件进行复用,持续做下去,不仅能够丰富自己的基础代码库,也能令后期的开发越来越便捷。

如此看来,现在经历的“枯燥”也许是有价值的,只不过它需要时间去验证、还需要一定的坚持才能见到回报。

wxWidgets似乎没有事件广播机制

wxWidgets的任意object产生了事件之后,似乎只能向父层传递,并且“一路向上”的往上传递。既不能向下传递、也不能横向传递。换言之,wxWidgets的事件是不能进行“广播”的。虽然没有通读过所有关于wxWidgets的文档,但是感觉上面的结论应该是准确的。

如果将整个窗口及上面所有的组件看做一颗“树”,任意节点上产生的事件,在“不能广播”的前提下,唯有这个节点的“父亲”是唯一的。

如果这个事件可以“向下传递”,那么任意节点下面的子节点有可能是多个,向下传递意味着就是“广播给所有的子节点”。

同理,如果能够横向传递,必然是先从当前节点向上传递给父节点,再由父节点广播给所有子节点,才能令兄弟节点收到消息。或者换一种想法,任意节点的兄弟节点也可能是多个,所以“横向传递”意味着有多个兄弟节点等待接收事件,由成了广播机制。

所以“没有广播能力”就意味着不能向子节点和兄弟节点发送事件。反之:不能向子节点发送通知就是在说不能进行广播。两种表述是一样的,就是不能只依靠事件对项目进行彻底的解耦开发。

没有广播机制,程序的解耦程度会大打折扣。我现在就遇到了好几处问题。总要迂回着完成事件的传递、或由“中间人”帮忙进行控制的操纵,代码写的凌乱不说、更重要的是这样“千回百转的羊肠小路”做控制的纽带,总担心日后会忘记它们之间的联系。

另一方面,这条“羊肠小径”无论多么的纵曲幽深,都一定不能中断。但是对于复杂的窗口应用程序、尤其是还在不断调整的开发阶段,页面上的控件总会不断地调整、新建、删除,不断的做出重新规划,每一次规划都要想着其中不知有多少“羊肠小径”需要连带着调整。既痛苦、又易出错。

虽然现在我用了一个全局单例来缓解问题,令代码可以尽可能的解耦;也缓解了组件的迁移需要不断调整众多“羊肠小径”的尴尬,但终归觉得还是不完美。此时此刻,这个痛苦的问题,着实令我有些疲倦。

wxWidgets框架下程序的正确终止方式

我正在基于wxWidgets3.2.5写一个小工具,这个小程序并非只有一个MyFrame,而是可能会在不同的场景中有不同的窗口被创建并显示出来、仅仅依靠Frame的Close按钮,是不能将程序真正结束掉的,即使有一些方法可以将程序终止掉,但用得不对则可能导致出现内存泄漏。虽然程序终止的时刻产生内存泄漏并不是什么问题,因为泄露的内存最终由系统回收了,但找到正确的程序终止方式,终归是有益无害的。

待续

update 2024.05.15:因为我最终决定放弃使用wxWidgets框架进行软件的开发,所以这篇文章的“待续”部分也就没有必要写了。

std::vector<T>.size()返回的是size_t类型

一、遇到的问题

今天写程序的时候代码运行崩溃,提示format specifier doesn’t match argument type。因为我刚刚接触C++,所以在这个问题上花了不少的时间。

代码如下:

void MyApp::wrong()
{
    std::vector<int> test;
    test.push_back(1);
    test.push_back(2);
    test.push_back(3);

    wxLogDebug("length is %d", test.size());

    for (int i = 0; i < test.size(); i++)
    {
        wxLogDebug("do something");
    }
}

代码非常的简单,但是一旦编译运行,就会出断言错误提示。最终发现原来vector<T>.size()返回的不是int类型,而是size_t类型,所以在wxLogDebug()中因为存在着断言,发现类型不同就报错了。而后面for()循环里面虽然不报错,那是因为for()循环里面相当于是对int < size_t进行判断,没有断言语句所以不报错,并不是因为真的没有错误。

(int < size_t)这个判断语句虽然在多数情况下都可以正确的执行,但安全起见,还是应该重新定义,改成for (size_t i=0; i<test.size(); i++)这样比较稳妥。

再回过头看wxLogDebug()里面正确的写法,就是使用%zu代替%d,这样格式化字符串的时候接受的参数类型就是size_t了。所以最终代码正确写法如下:

void MyApp::right()
{
    std::vector<int> test;
    test.push_back(1);
    test.push_back(2);
    test.push_back(3);

    wxLogDebug("length is %zu", test.size()); // 改动1

    for (size_t i = 0; i < test.size(); i++) // 改动2
    {
        wxLogDebug("do something");
    }
}

二、size_t和int有什么不同?

上面遇到的问题并不复杂,只是因为以前没有碰到过,所以浪费了一些时间。接下来的问题更令我感兴趣:size_t和int有什么不同吗?既然说size_t是无符号、且与当前系统最大可用内存匹配数,为什么不能直接用unsigned int代替呢?

原因是为了令代码在不同的平台上移植时更加的方便。size_t的确就是unsigned int,但具体是int16、int32还是int64并不一定,在不同的处理器、不同的系统上,size_t表示的范围是不同的。

例如在32位系统上,size_t是unsigned int32,而在64位系统上它的范围则是unsigned int64。

std::vector<T>.size()返回的虽然是size_t类型,但是更多的情形下,size_t并不是为了存储索引数值、而是为了存储内存地址,这个时候内存的地址最大能寻址到多少,size_t就能够跟随到多少。

虽然我们也可以直接定一个unsigned long int,也就是“无符号int64”,这样也可以确保在不同的系统下存储的数值都不会产生溢出,但是如果是在32位系统上编译这样的程序,就会产生严重的浪费,所有用到的变量,都有至少16位是永远用不到的,这是浪费存储空间、浪费内存、且效率低下的。

因而使用size_t更加的灵活,并且免除了我们在不同系统上编译代码时要考虑分配多少位给变量,完全交由编译器在预处理阶段自行根据系统的位数、预处理好了。

系统托盘菜单点击之后500毫秒内的残影

想用C++实现一个简单的屏幕截图工具的开发,工具程序是放在系统托盘(IconTray)中的,然后通过鼠标点击托盘图标弹出菜单,再点击菜单中的截屏菜单,完成截屏操作。

软件界面如下:

一旦全屏截屏或框选区域截屏点击之后,程序就开始运行,将当前的屏幕DC保存下来,然后借助memDC将屏幕DC复制到一个bitmap中,再完成写盘操作。看上去是个非常简单的需求。

但是在Windows系统中(Win10),这个icontray的弹出菜单在点击之后并不会立即消失,我这里感觉大约会有500ms的减淡消失效果,最终导致截屏保存下来的图像,总是会带有被点击菜单的残影,如下图:

在程序里面处理其实可以有很简单的方法,就是做一个延迟处理,例如我现在延迟了0.6s、也就是600ms之后在创建screenDC:

// 立即对当前屏幕进行全屏截屏操作
void MyApp::CaptureWholeScreen(wxCommandEvent&amp; event)
{
    wxString _imgSavePath = DatetimeUtil::getTodayScreenShotPath();
    if (PathUtil::dirExistOrMkdirSuccessful(_imgSavePath) != true)
    {
        wxLogDebug("截图存储目录不存在且无法创建,无法完成截图操作");
        return;
    }
 
    // 延迟0.6秒后将整个屏幕DC保存下来,待后面按显示器切割使用
    // @todo icontray的菜单点击后会渐变淡出,如果不设置这个0.6s的延迟,会有残影现象
    Sleep(600);
    wxScreenDC _screenDC;
 
    // 遍历每一个显示器对应的桌面,按各自桌面尺寸,从_screenDC上截取各个桌面的图像
    for (unsigned int _screenIndex = 0; _screenIndex &lt; wxDisplay::GetCount(); _screenIndex++)
    {
        wxRect _screenGeometry = wxDisplay(_screenIndex).GetGeometry();
        wxBitmap _screenCaptureBitmap(_screenGeometry.GetSize());
 
        wxMemoryDC _memDC;
        _memDC.SelectObject(_screenCaptureBitmap);
        _memDC.Blit(wxPoint(0, 0), _screenGeometry.GetSize(), &amp;_screenDC, _screenGeometry.GetPosition());
        _memDC.SelectObject(wxNullBitmap);
 
        wxString _filename = PathUtil::desktopTimestampFilename(_imgSavePath, _screenIndex);
        if (_screenCaptureBitmap.SaveFile(_filename, wxBITMAP_TYPE_PNG) == true)
        {
            // @todo 生成缩略图,并将图像的文件信息写入本地数据库
            // @todo 若软件主界面隐藏状态,则通知icontray弹出已生成截屏提示信息
        }
    }
}

这样就可以基本确保不会将菜单残影带进最终的截图图像中了。

但是这样的写法总是给人一种“怪怪”的感觉,心理上很难接受。它奇怪在并不是精确的控制,而是一个凭借感觉的控制——是所有的windows系统都这样吗?是所有的windows主题、所有的电脑CPU性能无论高低,都一定能在0.5s左右之后残影消失吗?

而且这个菜单残影是windows的视觉淡入淡出效果、还是我程序写的不好导致的?会不会有一个系统事件来通知我菜单真的已经彻底消失了?

不清楚,现在为了赶软件开发的进度,所以这里只能先简单粗暴的Sleep(600),等软件的整体功能全部完成之后,这里再返回头来深入的推敲一下吧。

小工具开发备忘

最近用C++语言写一个小的工具程序,进展还算顺利,大的方向和功能开展的比较顺利,但是细节上有很多小问题。为了确保软件开发的进度,我是抓大放小,先力求能将整体功能全部实现出来。

对于细节问题,只能是做些备注、放在一旁,等软件整体完成之后,再回过头来逐一推敲了。这里记录一下遇到的小问题,当是给自己的备忘,等以后有时间了,再慢慢研究。

1、《系统托盘菜单点击之后500毫秒内的残影》

这个问题并不严重,临时解决方案是在程序中触发截屏时,先延迟一段时间再开始进行截屏。

2、《std::vector.size()返回的是size_t类型》

这并不是“放在一旁、暂且不管”的缺陷,而是我在写代码的时候遇到的一个“引起程序崩溃”的bug。实际上并非真的导致了程序的崩溃,而是因为wxWidgets中的调试断言弹出了警告对话框。

只不过因为我对C++不熟悉、又这个弹出警告框的文字实在难于阅读,所以起初一直以为是程序崩溃了。找了好久才找到原因,并且发现只不过是在f(x)函数的调用是,参数x使用了int类型,而真正应该使用的是size_t类型。虽然int和size_t几乎一样,但严格的断言判定最终生成了警告信息。

这个问题虽然已经解决,但是额外的需要再补充一下细节知识的缺失——《C++中的int和size_t有什么不同?

3、《wxWidgets似乎没有事件广播机制

我现在对wxWidgets的认知来看,它是没有事件广播机制的,但是对于我现在正在开发的一个“相对复杂”的窗口,如果能够以事件广播,将会令代码更加简单。所以想到了一个也许笨拙的方法——使用全局单例——来临时解决问题。

================ 以下为尚未这里问题 ================

一、已解决的问题主要有:

1、为什么在程序退出时会产生一些内存泄漏?

答:之前使用的退出方式不正确,之前的退出方式在“程序退出”时,虽然也调用了析构函数,但是那个调用析构函数的时候,实际上内存已经泄露了。我在那个析构函数中进行资源释放,实际上是对已经泄露了内存、且资源句柄已经无效的时刻,才去释放资源,结果就是无权访问会引发程序崩溃。

按照之前的错误思路,怎么办呢?释放就会崩溃,所以只能选择不释放资源,这样程序就不会崩溃了。但是在debug调试器中,就会看到有内存泄漏产生。虽然这里产生的内存泄漏会再被系统回收处理,但是这相当于是将“烂摊子”交给了操作系统的内存控制器。

现在已经解决了。解决的方法就是改用正确的退出函数,这样正确的退出函数会触发正确的析构过程,在这个新的析构过程中,释放资源,此时的资源还没有泄露,也就可以正常的被释放掉。

2、icontray的弹出菜单文字的动态调整

这里只是一个小技巧而已:将mainFrame的指针传给iconTray,这样iconTray内部就能在弹出菜单之前,判断出mainFrame当前的状态,再根据mainFrame的当前状态实时生成菜单,就能够令弹出菜单的内容“实时”,使用体验会比较好。

二、尚未解决的问题:

1、程序多次被启动,虽然我用互斥锁能判断出来,但是第二次启动只能弹出一个提示“已有实例,不要重复启动”。

但是我想做的更好一些,使用IPC通信,第二次的启动可以通过IPC通知第一个实例,第一个实例如果正处于隐藏状态(icontray状态),就自己重新唤醒起来。

2、paintDC的绘制效率很低:已经使用双缓存和悬浮层两个方案,共同解决了这个问题。

3、wxSashLayoutWindow窗口的拖拽改变尺寸,产生的事件

OnSashDrag(wxSashEvent&)

这个事件折磨了我大约4个小时,后来才想起来打印看一下,结果发现这是在鼠标抬起的时候才唯一的产生一次响应。难怪无法实时改变窗口的大小。还没有解决思路。

经过三组RC网络相移之后的电压波动

一、问题的提出

有了前面的结论,可以知道电容电压跟随交流电源电压的关系,是满足下面的方程的:

\(V_{C}(t)=\frac{V_{m}}{\sqrt{1+(\omega RC)^2}} \times e^{\frac{-t}{RC}} + \frac{V_{m}}{\sqrt{1+(\omega RC)^2}} \times sin(\omega t-arctan(\omega RC))\)

1、这个方程中的 Vm / [(1+(wRC)^2)^(1/2)] 可以被视为A,这个A就是电容电压波形中的峰值。也就是说:在电源正弦的作用下,电容上的电压形成了一个新的正弦波(虽然并不是完整的正弦、起始瞬态略有畸变),这个新形成的“正弦波”的波峰是A;

2、方程中的-arctan(wRC)被视为相移角φ,也就是电容正弦错后于电源正弦的相移;

3、那么新的正弦波的频率呢?频率是不变的。它的频率是和电源频率相同的,只是波峰略低、相位略微向右偏移。

有了如上概念之后,就可以完成以下的两个问题的解答:

1、如何构建一个完美的60°相移,然后通过3组RC相移,完成180°的相移;

2、在进行了180°相移之后的第三组RC网络的输出电压,波峰是多大?也就是说最终反馈回控制极的电压有多少?

这篇博客,将完成上面2个问题的计算和整理。

二、为了实现60°的单组RC相移,重新进行相关元件的参数设置

电源电压峰值:12V。之所以要将电源电压定义为12V,目的是与Jack Kilby的实验相符。在我阅读的相关书籍中提到,他在实验室中用12V直流电开始实验,并在示波器上看到了令人激动的振荡波型;

电源频率:暂定100Hz。之所以要定义成100Hz,是为了方便绘图观察。后期可以重新调整这个频率;

电容:1uF,也就是0.000001F;

电阻:之前我在仿真中是直接使用的1000Ω,现在按照相移角为60°进行计算,电阻应该使用2760Ω。这里还有一个额外的问题:为什么之前仿真中,电容使用1kΩ也可以正常振荡呢?这个问题需要以后有时间了,再深入推敲一下;

如上的基本参数就定义好了,按照上面定义的参数,进行绘图,然后进行仿真,看绘图计算结果与仿真结果是否一致。

三、三组RC相移之后的最终波形及电压

其实有了上面的计算依据之后,不画图也可以计算出经过3次RC相移之后的最终输出电压峰值是多大了。因为经过一组RC之后的电压峰值是A,所以经过3组RC之后的电压峰值是A的三次方。因而,如果电源电压的峰值时12V,那么:

经过第一组RC之后的电压将会是6V左右;第二组RC之后的电压将是3V;第三组之后的电压将是1.5V。也就是说最终反馈回控制极的电压的波动将在+1.5V ~ -1.5V之间。

下面是使用SageMath完成的画图,从只有电源电压开始、一条条的增加新的电容电压曲线。需要注意的是要忽略掉曲线开始瞬态部分的失真情况:

这个开始部分的失真真令人苦恼!要知道我之所以花了很多时间《搞清楚电容电压跟随电源电压的数学公式和推导过程》,目的就是想在最终的SageMath生成准确正确的图像。而现在看来还是无法生成出正确的图像来。

很奇怪各类仿真软件在这里是怎么做的,为什么通过仿真软件的示波器,就能够看到正确的波形图呢?

四、仿真对比

待续

五、备忘

1、本文中使用到的LaTex公式,为了方便日后使用,源码备注如下:

V_{C}(t)=
\frac{V_{m}}{\sqrt{1+(\omega RC)^2}}
 \times
 e^{\frac{-t}{RC}}
+
\frac{V_{m}}{\sqrt{1+(\omega RC)^2}}
 \times
sin(\omega t-arctan(\omega RC))

2、本文中使用到的SageMath绘图语句,整理如下,方便日后使用:

Vm = 12
R = 2760
C = 0.000001
F = 100
omega = 2*pi*F

Vs(t) = Vm*sin(omega*t)
A = Vm / sqrt(1+(omega*R*C)^2)
Vc1(t) = A * exp(-t/(R*C)) + A * sin(omega*t-arctan(omega*R*C))

B = A /  sqrt(1+(omega*R*C)^2)
Vc2(t) = B * exp(-t/(R*C)) + B * sin(omega*t-arctan(omega*R*C)*2)

CC = B /  sqrt(1+(omega*R*C)^2)
Vc3(t) =  CC * exp(-t/(R*C)) + CC * sin(omega*t-arctan(omega*R*C)*3)
plot(Vs, 0, 0.03, color='black')
+ plot(Vc1, 0, 0.03, color='red')
+ plot(Vc2, 0, 0.03, color='green')
+ plot(Vc3, 0, 0.03, color='blue')

六、拾遗

1、此时此刻(2024年4月19日 16:07),这篇博客才写了一半,发现我在数学计算中用到的相移角似乎有些错误,而且数学计算时的模型和实际电路模型也存在着偏差,所以得到的SageMath结果和仿真结果并不一致。但是二者从数值上看是有相似性和吻合性的,所以应该只是一些小的偏差、错误导致的问题。再假以时日,应该能将相关错误找到,并得出正确的结果。

电容电压跟随电源电压的过程(2)

一、问题的提出

在上一篇博客《电容电压跟随电源电压的过程》中,基本搞清楚了电容的充电过程和放电过程电压的变化规律。但是它对我想了解的电路而言太基础了,根本无法指导我对当前电路的分析。

我更想搞清楚的是当电源为交流电时,电容电压的跟随情况,如下图所示:

图中的电源是100Hz的5V交流电,可以看到在电容上形成的电压的峰值(红线)略微低于5V,并且是滞后于电源电压的。这在网上已经有不少公式说明了它们之间的关系,但是却没有给出推导过程。

并且更为令我不安的,是这些现成的公式都是指电路进入“稳态”之后,而在上电的起始阶段,可以看出电容与电源之间的电压并不出现相位差。

所以在我想来,这个“现成的公式”是无法解答我的困惑的,所以这篇文章以及接下来的一段日子,我将尽可能搞清这个问题:电容电压是如何跟随交流电压的。

二、困扰

又陷入了“AI胡说”的困扰,这篇博客提出来的问题,我自认为问题定义的还是比较清晰的,但却找不到准确的文献。如果继续执着于利用搜索引擎或AI问答引擎,恐怕接下来要浪费的时间将是巨大的。

其实我觉得这个问题应该比较容易,它只是在解如下的方程:

\(\sum V(t) = U_{S}(t) + U_{C}(t) + U_{R}(t) = 0\)

这似乎没什么难度,但因为缺少相关的参考资料,所以我很难确定自己的推导过程每一步是否准确。尤其又因为我自己解出的答案,绘制成图形之后和预期的图形相差太远——因而可以确定我的求解是错误的。

有些不知该如何继续了。

三、找到了正确的计算公式、虽然并不完美

经过多次的搜索、查找,终于找到了一篇“正确的”参考资料:

https://www.iitp.ac.in/~siva/2016/ee101/Sinusoidal_Steady_State_Analysis.pdf

之所以可以认为这篇参考资料是正确的,是因为基于这篇资料计算电容电压,绘制出来的电容电压的波形是准确的(相移角准确、电容可以得到的最大电压准确、并且是稳定持续输出的):

这很不容易!我之前一个下午都在使用AI问答引擎找答案,结果给出来的各个计算公式都不正确。直到晚饭之后,都已经准备放弃了。但不甘心又找了一下,才找到上面这篇文档。

虽然这个文档给出的结论并不是我要的最终结论——它显然缺少系统上电的起始阶段的分析和曲线。但至少有了稳态之后的正确公式,可以先把后面的知识了解、掌握一下。

四、瞬态响应阶段和稳态阶段的完美波形

我感觉自己是找到“宝”了,上面的参考文档实际上是准确、完美的。只不过起初找到的时候没有细看,草草的照葫芦画瓢画了个初步的图形。细看之下,这篇文档实际上是含有初始瞬态响应阶段的分析的。按照最终的正确公式进行绘图,得到的图形正确、完美:

文档中给出来的推导公式也非常的简要明了。所以接下来只需要将文档中提及的公式好好学习、自行推导一遍,这一目的功课就应该算是完成了。

五、小节

1、其实上面的图形我完全可以在仿真软件里模拟出来,然后直接截屏就可以正常使用了。但是之所以纠结这个图形的产生过程,是因为我想搞清楚仿真软件中这个“电容电压跟随电源电压”的计算过程,究竟是怎样的;

2、如果不知道具体计算过程、不知道具体的推导过程,就很难理解为什么最终是RC的阻值、容值决定了它的相移。也无法自己利用SageMath绘制图形。不能自己绘制图形,就很难在博客文章或者“视频文章”中给出更详细的展现;

3、如今已经有了这个具体的“绘图公式”,我就可以按照自己之前的想法,绘制一系列的曲线,通过这些曲线说明为什么每一个RC网络可以实现(0°-90°)之间的相移、为什么必须需要至少3组RC网络才能形成180°的相移;

4、额外的,除了可以说明三组RC网络形成的180°相移外,还可以计算出最终反馈回去的电压有多少,与控制极的初始电压叠加之后形成的控制极电压波动有多大、进而推算出控制极的电流形成的开关效应,最终将Jack Kilby的RC Phase Shift Oscillator解释清楚了。

电容电压跟随电源电压的过程

一、问题的提出:

一颗有电且“满电”的电池,它的正极有着比负极更高的电势。

当将这颗“满电电池”的正负极通过导线连通时,通过电场力的作用,这条通路上的电子便会从低电势区域向高电势区域移动,目的是减小正负极之间的电势差,从而形成电子持续的定向移动,也就是电流。

因为这条导线是理想的、电池也是理想没有内阻的,所以电子的定向移动不会受到任何的阻碍,单位时间内通过导线横截面的电子数量可以无限多、电流无限大,只需近乎于0秒的时间,电池正负极的电势差就被拉成零、没有了电势差。

重新回到初始状态,在这条理想导线上加入一颗电阻,目的是阻碍电流的移动,使得导线上的电流不再是无限大,而是I=U/R,问:此种状态下,需要经过多少时间,电池的正负极电势差才能从U降低到0呢?

二、初步讨论

上面的问题中,一直用的是“电池”,目的是让问题更感性一些。其实这篇文章主要聊的是电容,因为电池和电容基本相似、可以类比,所以上面一直用“电池”来做表述。但电池和电容本质上是有差异的,所以为了更加严谨一些,下面不再使用“电池”的称呼,而是用“电容”来继续讨论。

一旦系统接通,电池(电容)将开始进行放电,电流经过电阻做工,并且逐渐的电容内部的电压会降低、直至电容电压降低到0。这也是上面提出来的问题:要经过多久的时间,电容电压才会从初始电压(例如5V),降低到0V呢?这里有一个值得注意的事情:电容初始电压虽然是5V、在系统导通的起始时刻,流经电阻(100Ω)的电流是I=5V/100Ω=50mA,但是这个电流并非恒定不变的。

随着时间的推移,电容内部的电场强度(如果是电池、则可以理解成电量)会越来越低、电容的端电压也会随之下降,从而导致流经电阻的电流也会越来越小。这个过程是连续的——每一个当前时刻的电流都是上一个时刻的电容电压决定的。在没有仔细的推敲之前,不妨先随手胡乱的画一画它们随时间t的变化关系:

如上的若干图形都是肆意画出来的,只有大概的猜测、并没有任何的理论依据。大方向上而言,电容电压随时间的推移一定是越来越低的,但究竟是上面图中哪一种形式呢?如果花更多一些时间思考,似乎能从“电能密度”的角度上猜测出,上面的3个曲线中,最右边的曲线似乎是更贴近于真实情况的。但也只是猜测。

另外,我们最关心的“经过了多长时间,电容电压变为0”,在上面的图形中也是无法推敲出来的。虽然我们知道这个时间一定非常小,所以图中都是“拍着脑袋决定”,在0.01s的时候,电容电压就已经降低到0V了,但实际情况究竟应该是怎样一个时刻?所以接下来就要从数学的角度具体推算一下了。

三、具体数学推导

电容放电过程中的电压变化,可以从两个维度上考虑,分别是KVL和KCL两个角度,也就是考虑电容上的电压时时刻刻等于电阻上的电压、或电容上流出的电流时时刻刻等于电阻上流过的电流。这两种思考角度本质上是一样的,具体的推到过程由下面的两个参考资料中可以详尽的看明白:

1、以电压考虑,依据Kirchhoff’s voltage law (KVL)进行推导:

参考资料:https://web.mit.edu/sahughes/www/8.022/lec09.pdf

2、以电流考虑,依据Kirchhoff’s current law (KCL)进行推导:

参考资料:https://mechatrofice.com/circuits/charging-capacitor-derivation

3、额外的,一篇不错的科普概述文章:

参考资料:https://courses.lumenlearning.com/suny-physics/chapter/21-6-dc-circuits-containing-resistors-and-capacitors/

这里直接给出最终的结论,电容上的电压随时间T的关系式是:

\(V_{C}(t) = V_{0} \times e^{-\frac{t}{RC}}\)

由此看出电容电压在数学上是永远不会等于0的,它只会越来越趋近于0。但是现实世界中,因为电路上的IC并非理想的、并且存在着热扰动,所以只要经过一段时间,电容趋近于0的电压就会因为一个“小的波动”而抵达X轴,从而时电压归零。

四、后记

1、写此篇博客的目的和智能问答AI:

写这篇博客目的是熟悉一下各个辅助工具(例如SageMath、LaTex、LTspice等)的使用方法,以便后面的博客写作更加熟练。

借助于当下流行的人工AI问答工具,节省了大量的搜索和推算时间。人工AI问答工具是把“双刃剑”,它的回答正确时的确可以方便的找到答案,主要是在不会提问的时候,可以通过几次提问,令自己逐渐找到提问的方式、方向,并且让自己知道应该问什么词语。

但是对于AI给出的答案,则是“五花八门”,甚至于有些时候它就是信口开河,可谓是一本正经的胡说八道。明明是错误的推导,也被AI说的有模有样,十分迷惑人。这个时候就是最浪费时间的,思路被它带偏不说,还会花大量的时间以为是自己的问题。

2、电容的充放电过程:

本文暂且指整理出了电容的放电过程,对于充电并没有讨论。但是只要对放电过程的数学公式有了清晰的了解,充电过程也就可以容易的理解并掌握了。

3、直流电压下电容电压的跟随:

本文讨论的是电容自己做为“电源”对外放电。但实际上我更关心的是系统内有一个直流电源,而电容视为“被充电”元件时,电容的电压表现。所以虽然说过程相反、原理相似,也容易理解,但还是要再花时间另开一篇博客学习、整理。

4、交流电压下电容电压的跟随:

之所以要完成“在直流电压下电容的充电过程”的学习,是因为我最终的目的是学习、掌握当电源是“正弦交流电”时,电容的电压同电源电压的跟随情形。所以这里也是一个知识点的欠缺。并且现在感觉,这个问题将会更加复杂一些,尤其是在数学推导上,也许存在着更高深的数学知识,需要从基础学起。

5、最终目的:

只有在完成了上面提到的电容电压随交流电源跟随的情况,会计算、会画图之后。才能将RC Phase Oscillator振荡器中,为什么会有3组RC相移网络的事情搞清楚、说明白,所以这篇文章实际上只是之前学习Jack Kilby的RC相移振荡器的基础中的基础。

RC振荡电路初学笔记(3)

这是一篇临时的备忘文章,主要是将最近几天对RC振荡器的学习进展做一个简单的罗列。虽然已经花了不少时间在这个电路上,但是我对这个电路的细节还有很多的欠缺。好在最令我困惑的几个问题,已经基本搞清楚了:

1、在这个振荡电路中,使用相移器的目的是什么?

答:这个问题我现在“隐约”有了答案,但是还需要对当前的认知进行更深入的学习,以便从数学公式上对相关的“相移量”有个更加准确的认识、并且要能通过参数计算出具体的结果。尤其是我现在所初步感受到的“电容电压跟随源电压”,它的跟随程度和如何控制,要学会具体的计算过程;

2、为什么要使用3组RC相移网络?

答:这个问题现在我已经可以通过画图的方法得出答案了。这个问题的答案的得来对我而言十分的不易:自己既没有夯实的基础知识、网络上又没有比较明确的答案。幸好最后自己想出了答案。虽然暂时没有办法印证我当前的答案是否标准、正确,但感觉应该基本是靠谱的。为了让答案更容易理解,这个知识点要配合一些画图才能完成。所以接下来将学习使用SageMath将相关的正弦波形绘制出来;

3、电容的容抗的本质是什么?

答:还没有搞清楚;

4、NPN为什么是电流控制型元件?

答:这个问题似乎也有了一个初步的答案。不过也还没有将答案从头梳理、逐一印证。所以还需要一定的时间完成这部分知识的整理、证实。并且在确定自己的想法就是书本上表述的意思之后,整理成博客文章,发在这个系列文集之中。

5、我当前绘制的RC相移振荡器电路,起振阶段的工作原理是怎样的?

答:不确定,不了解。因为我现在使用的2款仿真软件对于起振阶段的仿真并不相同,所以也说不清哪一个软件的起振阶段描述是准确的。因为无法确定哪一个表现准确,也就无法去“打哪儿指哪儿”的想它的起振过程。所以这个起振阶段的起振过程,还要再花时间推敲;

6、我当前绘制的RC相移振荡器电路,是否真实、准确?

答:这是相比Jack Kilby的论文而言的。他的论文中对于RC相移部分使用了一个很简略的画法。现在不确定他的简略画法是:a、一坨电容覆盖在一坨电阻上面,就是与3组RC网络等效的;b、其实简略画法要展开成3组RC网络才可以;c、在他的真实电路实现内部,还有其它未知的细节;以上哪种情况呢?

以上,就是现在我对RC相移振荡器的学习进展。如此看来,已经学习过的内容还没有整理成文章、大体了解了的内容还没有深入探究、尚未了解的内容还有很多……如此看来,这个系列的学习工作,还是有很多事情要继续的。