<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[爱折腾的孩纸]]></title><description><![CDATA[爱折腾的孩纸]]></description><link>https://zhujinliang.com/</link><image><url>https://zhujinliang.com/favicon.png</url><title>爱折腾的孩纸</title><link>https://zhujinliang.com/</link></image><generator>Ghost 2.7</generator><lastBuildDate>Thu, 12 Jun 2025 10:10:19 GMT</lastBuildDate><atom:link href="https://zhujinliang.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[编译TFLite静态库]]></title><description><![CDATA[<p>首先，为什么要自己动手编译，以及为什么要编译成静态库，初衷暂且不表，罗列一下编译过程。</p>
<p>如果你需要动态链接库的话，网上有大把以及编译好的。</p>
<p>另外，千万不要用windows下的编译工具，比如Mingw，以及Msys2，各种问题纯浪费了我一天的时间。使用Ubuntu交叉编译比直接在windows上编译更靠谱。</p>
<ul>
<li>
<p>安装编译工具</p>
<p>安装 GCC 或者 mingw 用来交叉编译 windows 平台版本</p>
<p><code>sudo apt install mingw-w64</code></p>
<p>如果使用，注意 mingw 应选择 posix 版本，执行以下命令可切换版本：</p>
<p><code>sudo update-alternatives --config x86_64-w64-mingw32-gcc</code><br>
<code>sudo update-alternatives --config x86_64-w64-mingw32-g++</code></p>
</li>
<li>
<p>创建目录，用于下载 tensorflow 源码，例如 <code>./tensorflow-2.6.5</code></p></li></ul>]]></description><link>https://zhujinliang.com/bian-yi-tflist/</link><guid isPermaLink="false">667d4a18a40233043074f68c</guid><dc:creator><![CDATA[zhujinliang]]></dc:creator><pubDate>Thu, 12 Jun 2025 09:55:50 GMT</pubDate><content:encoded><![CDATA[<p>首先，为什么要自己动手编译，以及为什么要编译成静态库，初衷暂且不表，罗列一下编译过程。</p>
<p>如果你需要动态链接库的话，网上有大把以及编译好的。</p>
<p>另外，千万不要用windows下的编译工具，比如Mingw，以及Msys2，各种问题纯浪费了我一天的时间。使用Ubuntu交叉编译比直接在windows上编译更靠谱。</p>
<ul>
<li>
<p>安装编译工具</p>
<p>安装 GCC 或者 mingw 用来交叉编译 windows 平台版本</p>
<p><code>sudo apt install mingw-w64</code></p>
<p>如果使用，注意 mingw 应选择 posix 版本，执行以下命令可切换版本：</p>
<p><code>sudo update-alternatives --config x86_64-w64-mingw32-gcc</code><br>
<code>sudo update-alternatives --config x86_64-w64-mingw32-g++</code></p>
</li>
<li>
<p>创建目录，用于下载 tensorflow 源码，例如 <code>./tensorflow-2.6.5</code></p>
<p><code>wget https://github.com/tensorflow/tensorflow/archive/refs/tags/v2.6.5.zip</code><br>
<code>unzip v2.6.5.zip</code></p>
</li>
<li>
<p>创建目录，用于存放 CMakeFiles，例如 <code>./tflite-build</code></p>
<p><code>mkdir tflite-build</code><br>
<code>cd tflite-build</code></p>
</li>
<li>
<p>初始化 CMake</p>
<p>不交叉编译</p>
<pre><code>cmake -DTFLITE_C_BUILD_SHARED_LIBS:BOOL=OFF \
      ../tensorflow-2.6.5/tensorflow/lite/c
</code></pre>
<p>交叉编译</p>
<pre><code>export CC_PREFIX=x86_64-w64-mingw32-
cmake -DCMAKE_C_COMPILER=${CC_PREFIX}gcc \
      -DCMAKE_CXX_COMPILER=${CC_PREFIX}g++\
      -DTFLITE_C_BUILD_SHARED_LIBS:BOOL=OFF \
      -DCMAKE_SYSTEM_NAME=CYGWIN \
      -DCMAKE_SYSTEM_PROCESSOR=x86_64 \
      ../tensorflow-2.6.5/tensorflow/lite/c
</code></pre>
</li>
<li>
<p>交叉编译过程中会遇到几个错误：</p>
<p>一个是找不到 <code>&lt;byteswap.h&gt;</code> 头文件，编辑 <code>farmhash/src/farmhash.cc</code> 文件，在 172 行附近，注释掉 <code>#include &lt;byteswap.h&gt;</code> 语句，并添加以下两行：</p>
<pre><code>#define bswap_32(x) __builtin_bswap32 (x)
#define bswap_64(x) __builtin_bswap64 (x)
</code></pre>
<p>另一个是找不到 <code>&lt;sys/mman.h&gt;</code> 头文件，这是因为 mingw 没有mmap的实现。<br>
这里我找了一个项目，<a href="https://github.com/alitrack/mman-win32">https://github.com/alitrack/mman-win32</a> ，复制其中的 <code>mman.c</code> 和 <code>mman.h</code> 到 <code>./tensorflow-2.6.5</code> 下的 <code>/tensorflow/lite/</code> 目录中。然后修改<code>mmap_allocation.cc</code> 文件，使用 <code>&quot;mman.h&quot;</code> 代替 <code>&lt;sys/mman.h&gt;</code>。</p>
<p>还有一个最后编译自己的程序链接时的错误：<br>
<code>/usr/bin/x86_64-w64-mingw32-ld: ./tflite-build/output/libcpuinfo.a(init.c.o):init.c:(.text+0x117): undefined reference to 'max'</code><br>
编辑 <code>cpuinfo-source/src/x86/windows/init.c</code> 文件，在文件中添加一个max函数，比如：</p>
<pre><code>static inline DWORD max(DWORD a, DWORD b) {
        return a &gt; b ? a : b;
}
</code></pre>
</li>
<li>
<p>准备好后可以尝试执行编译了</p>
<p><code>cmake --build . -j 1</code></p>
<p>建议初次编译使用单线程，以便发生错误时可以清楚的看到错误原因。</p>
</li>
<li>
<p>拷贝编译结果到 ouptput 目录，这个操作是为了使用时方便链接</p>
<p>创建一个 ouptput 目录，将静态库拷贝到该目录中，注意，当使用静态库时，需要链接这里的多个静态库，只链接<code>libtensorflowlite_c.a</code>是不够的。</p>
<pre><code>cp libtensorflowlite_c.a output/
cp _deps/flatbuffers-build/libflatbuffers.a output/
cp _deps/farmhash-build/libfarmhash.a output/
cp _deps/ruy-build/libruy.a output/
cp _deps/xnnpack-build/libXNNPACK.a output/
cp _deps/fft2d-build/libfft2d_fftsg2d.a output/
cp _deps/fft2d-build/libfft2d_fftsg.a output/
cp tensorflow-lite/libtensorflow-lite.a output/
cp pthreadpool/libpthreadpool.a output/
cp cpuinfo/libcpuinfo.a output/
cp clog/libclog.a output/
</code></pre>
<p>头文件在源码目录中。</p>
<p>但是用头文件编译 Mingw 版 windows 应用时，可能会遇到 <code>undefined reference to `__imp_xxx</code> 错误提示，这是因为 <code>lite/c/c_api_types.h</code> 头文件中的 <code>TFL_CAPI_EXPORT</code> 定义有问题，tflite 未提供 windows 平台的静态链接库支持，故判断平台为 windows 后，直接追加了 <code>#define TFL_CAPI_EXPORT __declspec(dllexport)</code> 定义，使得编译器链接时认为应该在动态链接库中进行查找。</p>
<p>一个粗暴的解决办法是删除<code>lite/c/c_api_types.h</code> 头文件中 35 至 43 行的内容，随后增加一行 <code>#define TFL_CAPI_EXPORT ;</code>。</p>
</li>
<li>
<p>以下是 Golang 编译 <code>github.com/mattn/go-tflite</code> 并静态链接 tensorflowlite_c 的使用例子：</p>
<p><code>export CGO_CFLAGS=-I./tensorflow-2.6.5</code><br>
<code>export CGO_LDFLAGS=&quot;-L./tflite-build/output -ltensorflow-lite -lruy -lXNNPACK -lfft2d_fftsg -lfft2d_fftsg2d -lflatbuffers -lfarmhash -lpthreadpool -lcpuinfo -lclog -lstdc++ -lm&quot;</code><br>
<code>go build -o gotflite-app</code><br>
或者<br>
<code>export CC=/usr/bin/x86_64-w64-mingw32-gcc</code><br>
<code>GOOS=windows go build -ldflags '-extldflags &quot;-static&quot;' -o gotflite-app.exe</code></p>
<p>建议使用绝对路径替换上面命令中的路径。<br>
windows 平台建议使用 -extldflags &quot;-static&quot; 链接参数编译，否则直接运行会提示缺少 glibc 等 DLL。</p>
</li>
</ul>
<p>最后是我当前使用的 GCC 版本：</p>
<pre><code>gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/11/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 11.4.0-1ubuntu1~22.04' --with-bugurl=file:///usr/share/doc/gcc-11/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++,m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-11 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-11-XeT9lY/gcc-11-11.4.0/debian/tmp-nvptx/usr,amdgcn-amdhsa=/build/gcc-11-XeT9lY/gcc-11-11.4.0/debian/tmp-gcn/usr --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-serialization=2
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 11.4.0 (Ubuntu 11.4.0-1ubuntu1~22.04)
</code></pre>
<pre><code>x86_64-w64-mingw32-gcc -v
Using built-in specs.
COLLECT_GCC=x86_64-w64-mingw32-gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-w64-mingw32/10-posix/lto-wrapper
Target: x86_64-w64-mingw32
Configured with: ../../src/configure --build=x86_64-linux-gnu --prefix=/usr --includedir='/usr/include' --mandir='/usr/share/man' --infodir='/usr/share/info' --sysconfdir=/etc --localstatedir=/var --disable-option-checking --disable-silent-rules --libdir='/usr/lib/x86_64-linux-gnu' --libexecdir='/usr/lib/x86_64-linux-gnu' --disable-maintainer-mode --disable-dependency-tracking --prefix=/usr --enable-shared --enable-static --disable-multilib --with-system-zlib --libexecdir=/usr/lib --without-included-gettext --libdir=/usr/lib --enable-libstdcxx-time=yes --with-tune=generic --with-headers --enable-version-specific-runtime-libs --enable-fully-dynamic-string --enable-libgomp --enable-languages=c,c++,fortran,objc,obj-c++,ada --enable-lto --enable-threads=posix --program-suffix=-posix --program-prefix=x86_64-w64-mingw32- --target=x86_64-w64-mingw32 --with-as=/usr/bin/x86_64-w64-mingw32-as --with-ld=/usr/bin/x86_64-w64-mingw32-ld --enable-libatomic --enable-libstdcxx-filesystem-ts=yes --enable-dependency-tracking SED=/bin/sed
Thread model: posix
Supported LTO compression algorithms: zlib
gcc version 10-posix 20220113 (GCC)
</code></pre>
<p></p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[可承受40V输入的DCDC芯片]]></title><description><![CDATA[<p>这两个芯片可以在SOT23-6封装下实现最高40V电源供电系统，可在24V工控系统或远程供电的传感器上使用</p>
<ul>
<li>TI的LMR14010ADDCR</li>
<li>矽力杰的SY8401ABC/SY8291ABC</li>
</ul>
<p>两者封装和引脚定义是相同的，但反馈电压不同，代换时需要调整反馈电阻</p>
<p>价格方面，LMR14010立创4.9元，优信4.6元；SY8401立创4.41元，优信2.7元；SY8291立创1.92元，优信1.35元。<br>
电路设计可参考TI的Datasheet，电感使用22uH，使用SY8401代换时，只需要根据反馈电压不同，更换反馈电阻即可。</p>
<p>两者详细参数对比：</p>
<table>
<thead>
<tr>
<th>项目</th>
<th>LMR14010</th>
<th>SY8401</th>
<th>SY8291</th>
</tr>
</thead>
<tbody>
<tr>
<td>最大输入电压</td>
<td>40V(45V瞬时)</td>
<td>50V(极限55V)</td>
<td>40V(极限42V)</td>
</tr>
<tr>
<td>最大电流</td>
<td>1A</td>
<td>0.8A</td>
<td>1.2A</td>
</tr>
<tr>
<td>开关频率</td>
<td>0.7M</td>
<td>1.2M</td>
<td>0.</td></tr></tbody></table>]]></description><link>https://zhujinliang.com/ke-cheng-shou-40v/</link><guid isPermaLink="false">64398b5da40233043074e09a</guid><dc:creator><![CDATA[zhujinliang]]></dc:creator><pubDate>Wed, 09 Aug 2023 15:47:44 GMT</pubDate><content:encoded><![CDATA[<p>这两个芯片可以在SOT23-6封装下实现最高40V电源供电系统，可在24V工控系统或远程供电的传感器上使用</p>
<ul>
<li>TI的LMR14010ADDCR</li>
<li>矽力杰的SY8401ABC/SY8291ABC</li>
</ul>
<p>两者封装和引脚定义是相同的，但反馈电压不同，代换时需要调整反馈电阻</p>
<p>价格方面，LMR14010立创4.9元，优信4.6元；SY8401立创4.41元，优信2.7元；SY8291立创1.92元，优信1.35元。<br>
电路设计可参考TI的Datasheet，电感使用22uH，使用SY8401代换时，只需要根据反馈电压不同，更换反馈电阻即可。</p>
<p>两者详细参数对比：</p>
<table>
<thead>
<tr>
<th>项目</th>
<th>LMR14010</th>
<th>SY8401</th>
<th>SY8291</th>
</tr>
</thead>
<tbody>
<tr>
<td>最大输入电压</td>
<td>40V(45V瞬时)</td>
<td>50V(极限55V)</td>
<td>40V(极限42V)</td>
</tr>
<tr>
<td>最大电流</td>
<td>1A</td>
<td>0.8A</td>
<td>1.2A</td>
</tr>
<tr>
<td>开关频率</td>
<td>0.7M</td>
<td>1.2M</td>
<td>0.8M</td>
</tr>
<tr>
<td>反馈电压</td>
<td>0.765V</td>
<td>0.6V</td>
<td>0.6V</td>
</tr>
</tbody>
</table>
<p>另外TPS54302/TPS54202/TPS563200/TPS562200/TPS563201可互相替换，为17V或18V输入，2A或3A输出电流，同步降压DCDC，但需要调整反馈电组以及电感。其中TPS563201要求较低，可在2.2uH电感和20-68uF电容下工作</p>
<p>另有SY8120/SY8113B，输入电压最高18V，同步降压DCDC</p>
]]></content:encoded></item><item><title><![CDATA[STM32压缩编译产生的固件大小的一些经验]]></title><description><![CDATA[<p>最近打算做一个引导程序，作用就是将一个 FLASH 扇区作为分区表，然后选择一个程序进入。该引导程序当然越小越好，最终我把程序大小压缩到了 1KB 以内，这里介绍一些方法。</p>
<p>首先使用 STM32CubeMX 生成一个项目，不要使用 HAL 库。</p>
<h4 id="v616">使用V6.16编译器</h4>
<p>V6编译器优化选项有 <code>-Oz image size</code> 优化，这个可以进一步减少体积</p>
<h4 id="">精简中断向量表</h4>
<p>如果你的程序不需要使用中断，最多可以将中断向量减少到只有 4 个，这 4 个是不可屏蔽中断，最好不要去掉，以免程序跑飞。</p>
<p>在 <code>startup_xxx.s</code> 文件中，找到 <code>__Vectors</code> 标签，删掉除了 <code>__initial_sp</code>、<code>Reset_Handler</code>、<code>NMI_Handler</code>、<code>HardFault_</code></p>]]></description><link>https://zhujinliang.com/ya-suo-stm/</link><guid isPermaLink="false">643d78e9a40233043074e0f7</guid><dc:creator><![CDATA[zhujinliang]]></dc:creator><pubDate>Wed, 09 Aug 2023 15:38:32 GMT</pubDate><content:encoded><![CDATA[<p>最近打算做一个引导程序，作用就是将一个 FLASH 扇区作为分区表，然后选择一个程序进入。该引导程序当然越小越好，最终我把程序大小压缩到了 1KB 以内，这里介绍一些方法。</p>
<p>首先使用 STM32CubeMX 生成一个项目，不要使用 HAL 库。</p>
<h4 id="v616">使用V6.16编译器</h4>
<p>V6编译器优化选项有 <code>-Oz image size</code> 优化，这个可以进一步减少体积</p>
<h4 id="">精简中断向量表</h4>
<p>如果你的程序不需要使用中断，最多可以将中断向量减少到只有 4 个，这 4 个是不可屏蔽中断，最好不要去掉，以免程序跑飞。</p>
<p>在 <code>startup_xxx.s</code> 文件中，找到 <code>__Vectors</code> 标签，删掉除了 <code>__initial_sp</code>、<code>Reset_Handler</code>、<code>NMI_Handler</code>、<code>HardFault_Handler</code> 以外的 DCD 表项。</p>
<p>进入程序第一步执行 <code>__disable_irq();</code>，以免进入其它错误中断或外设中断。</p>
<p>对比 stm32f103 原本有 59 个中断向量，此项精简可减少 220 字节容量。</p>
<p>另外，<code>startup_xxx.s</code> 文件中有内核中断的代码块，可以一起删掉，同时NMI_Handler 和 HardFault_Handler 一般也不会使用到，可以将这两项也指向 Default_Handler，该函数进入后会进入死循环。</p>
<p>将中断向量表放到内存中对于程序瘦身一般作用不大，生成中断向量表需要的代码量不见的比直接放到 Flash 上小。程序中使用轮询方式即可。</p>
<h4 id="">精简启动初始化</h4>
<p>查看 Reset_Handler，在 <code>startup_xxx.s</code> 文件中，会先执行 <code>SystemInit</code>，然后执行 <code>__main</code>。</p>
<p>SystemInit 一般没有什么作用，如果我们使用外部 SRAM 以及自定义了中断向量表地址，会在这里进行初始化。</p>
<p>这里可以直接删除 Reset_Handler 函数，将中断向量表中的 Reset_Handler 记录直接指向 <code>__main</code>。</p>
<h4 id="gpio">精简 GPIO 和外设初始化</h4>
<p>首先来说，使用 xxx_InitStruct 和 LL_xxx_Init 函数生成的代码最大。我们可以不使用该种方式，而是使用 LL 库提供的设置寄存器函数进行初始化，以 GPIO 为例。</p>
<pre><code>LL_GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = LL_GPIO_PIN_2;
GPIO_InitStruct.Mode = LL_GPIO_MODE_ALTERNATE;
GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
LL_GPIO_Init(GPIOA, &amp;GPIO_InitStruct);
</code></pre>
<p>上面的代码可以写成：</p>
<pre><code>LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_2, LL_GPIO_MODE_ALTERNATE);
LL_GPIO_SetPinSpeed(GPIOA, LL_GPIO_PIN_2, LL_GPIO_SPEED_FREQ_HIGH);
LL_GPIO_SetPinOutputType(GPIOA, LL_GPIO_PIN_2, LL_GPIO_OUTPUT_PUSHPULL);
</code></pre>
<p>LL_GPIO_Init 函数中有不少判断，实际没有必要，反而增大代码尺寸。</p>
<p>对于其它外设的初始化也是同样的道理。</p>
<p>然后，即便如此，也不如直接操作寄存器代码量少。我们在启动后初始化外设环节，通常来说不用考虑外设当前的状态，将对应的寄存器值改为我们需要的值即可，而且这样可以同时修改多个属性。但是注意有的外设设置寄存器时要求有先后顺序，或者只有在外设关闭时才可修改某些值。</p>
<p>比如说初始化 UASRT 可以写成：</p>
<pre><code>USART2-&gt;CR1 = LL_USART_DATAWIDTH_8B | LL_USART_PARITY_NONE | LL_USART_DIRECTION_RX;
USART2-&gt;CR2 = LL_USART_STOPBITS_1;
USART2-&gt;CR3 = LL_USART_HWCONTROL_NONE;
LL_USART_SetBaudRate(USART2, 8000000UL, 115200);

LL_USART_Enable(USART2);
</code></pre>
<p>如果不知道寄存器应设置为什么值，可以进调试，打断点在外设初始化结束处，然后使用 System Viewer 查看外设的寄存器值。</p>
<p>上面展示的代码省略了 APB/AHB EnableClock 过程。</p>
<h4 id="systemclock_config">精简 SystemClock_Config</h4>
<p>接着来到 main.c，找到 <code>SystemClock_Config</code>，首先对 LL_Init1msTick 和 LL_SetSystemCoreClock 动手。</p>
<p>LL_Init1msTick 作用是初始化 SysTick。如果我们没有用到 SysTick，直接注释掉就行。即便用到了，直接操作寄存器进行初始化可以省掉不少的代码。</p>
<p>LL_SetSystemCoreClock 作用是将系统时钟保存到一个全局变量 <code>SystemCoreClock</code> 中，其它外设初始化时可能会用到这个值。但是我们既然打算操作寄存器来初始化，这个也没有什么作用，可以注释掉。变量 <code>SystemCoreClock</code> 定义在 system_stm32xxx.c 中，这个可以不用管他。</p>
<p>SystemClock_Config 中初始化 FLASH 和 RCC 的过程也可以直接操作寄存器。以下是使用内部 8MHz HSI 的代码示例：</p>
<pre><code>// LL_FLASH_SetLatency(LL_FLASH_LANTENCY_0);
FLASH-&gt;ACR = LL_FLASH_LATENCY_0 | FLASH_ACR_PRFTBE;
while(LL_FLASH_GetLatency() != LL_FLASH_LATENCY_0)
{
}

// LL_RCC_HSI_SetCalibTrimming(16);
// LL_RCC_HSI_Enable();
RCC-&gt;CR = RCC_CR_HSION | (16 &lt;&lt; RCC_CR_HSITRIM_Pos);

/* Wait till HSI is ready */
while(LL_RCC_HSI_IsReady() != 1)
{
}

//LL_RCC_SetAHBPrescaler(LL_RCC_SYSCLK_DIV_1);
//LL_RCC_SetAPB1Prescaler(LL_RCC_APB1_DIV_1);
//LL_RCC_SetAPB2Prescaler(LL_RCC_APB2_DIV_1);
//LL_RCC_SetSysClkSource(LL_RCC_SYS_CLKSOURCE_HSI);
RCC-&gt;CFGR = LL_RCC_SYSCLK_DIV_1 | LL_RCC_APB1_DIV_1 | LL_RCC_APB2_DIV_1 | LL_RCC_SYS_CLKSOURCE_HSI;

/* Wait till System clock is ready */
while(LL_RCC_GetSysClkSource() != LL_RCC_SYS_CLKSOURCE_STATUS_HSI)
{
}
</code></pre>
<h4 id="ll_apbx_gprx_enableclock">LL_APBx_GPRx_EnableClock函数</h4>
<p>LL库里对这两个函数以及AHB1_GRP1_EnableClock实现存在一个问题，LL库的实现是这样的：</p>
<pre><code>__STATIC_INLINE void LL_AHB1_GRP1_EnableClock(uint32_t Periphs)
{
    __IO uint32_t tmpreg;
    SET_BIT(RCC-&gt;AHBENR, Periphs);
    /* Delay after an RCC peripheral clock enabling */
    tmpreg = READ_BIT(RCC-&gt;AHBENR, Periphs);
    (void)tmpreg;
}
</code></pre>
<p>问题在注释后面一句，这里通过读取一次寄存器内容，确保外设时钟已开启。这个操作在 ST 的 errata es0222 里也有说明，是官方提供的一种 workaround。</p>
<p>但这里实现用的是 READ_BIT，该操作会多一个 &amp; 操作，是不必要的。将这句改成 <code>tmpreg = RCC-&gt;AHBENR;</code>，同时该函数是 inline 的，而且 EnableClock 很可能会调用多次，可以节省一些空间。</p>
<p>另外 <code>(void)tmpreg</code> 在 V6.16 编译器上也是不必要的。<code>tmpreg = xxx</code> 会产生两条语句，将外设地址的值读到通用寄存器，然后放入栈（因为tmpreg是在栈上），而 <code>(void)tmpreg</code> 又会从栈上取出值放到通用寄存器上。入栈也不是必要的，但这是编译器行为，我们不好调整。</p>
<h4 id="">使用传参代替全局变量</h4>
<p>我的实验发现，如果程序逻辑不复杂，构造一个结构体，通过传结构体指针，让变量在栈上，此时可通过 SP+偏移 进行地址访问，比放在全局变量中生成的代码要小。</p>
<h4 id="">去除分散加载代码</h4>
<p>查看汇编代码，发现该程序并未使用到分散加载，但仍然生成了相关的代码和数据块。<code>__main</code> 函数实现了3个功能：</p>
<ol>
<li>设置 SP 指针</li>
<li>调用 <code>_main_scatterload</code></li>
<li>调用 <code>__main_after_scatterload</code></li>
</ol>
<p><code>_main_scatterload</code> 会从一个数据表开始读取数据，并调用表中记录的函数地址。查看该表只有一个结束的标志，<code>_main_scatterload</code>不会有任何效果。<br>
<code>__main_after_scatterload</code> 则直接进入 main 函数。</p>
<p>我们可以通过修改启动文件 <code>startup_xxx.s</code>，移除部分代码。经测试，<code>__main</code> 是不能覆盖的，我们可以对这里面最复杂的 <code>__scatterload</code> 函数动手，写一个函数覆盖原本的实现，让其直接跳转到 <code>__main_after_scatterload</code>。代码如下：</p>
<pre><code>__scatterload   PROC
                EXPORT  __scatterload
                IMPORT  __main_after_scatterload
                
                BL.W    __main_after_scatterload
                ENDP
</code></pre>
<h4 id="">合并相似函数</h4>
<p>举例程序里有计算 CRC 和拷贝内容两个函数，这两个函数都有循环这一相似操作，可以考虑做成一个函数来完成两种功能，该函数输入源地址和目标地址，以及长度，返回校验和。当只需要拷贝时，忽略返回结果即可，当只需要计算 CRC 时，传入相同的源地址和目标地址，让其做原地拷贝。代价是上述两种操作都会有性能浪费，但只要整体耗时满足我们的要求即可。</p>
<h4 id="">结论</h4>
<p>最终实现了一个程序，程序在启动后，会初始化 SysTick 以及 USART2 用于接收选择启动分区的指令，之后会依次检索其后的 FLASH 内容，读取每条分区记录，通过比对分区记录设置的延时时间和启动“密码”，选择启动分区，进行 CRC32 校验并引导该分区的应用程序。</p>
<p>实际场景中我们可以将第一个分区设置为应用，引导地址指向 0x80000800，第二个分区设置为 OTA 程序，根据 OTA 程序大小，放置在 FLASH 末尾。应用程序设置延时为 3 秒，OTA 程序设置延时为 0，设置启动“密码”和分区 1 启动失败标志，这样在接收到对应的“密码”后或应用分区启动失败（例如CRC校验错误）后会立即进入 OTA 程序。启动密码可以通过串口输入，或者读取内存中固定地址的内容，以实现应用程序跳转入 OTA 程序功能。</p>
<p>程序使用轮询方式查询外设状态接收数据，设置栈大小为 512 字节，未使用任何全局变量。</p>
<p>编译后 <code>Code=996 RO-data=16 RW-data=0 ZI-data=512</code>，ROM 大小为 1012 字节，刚好可以放到一个扇区中。</p>
<p>后续可以考虑将代码容量放宽到 2KB，添加一个解压程序，实现将 FLASH 内容解压后放置在内存指定位置然后再进行引导的功能。</p>
]]></content:encoded></item><item><title><![CDATA[C# nQuant.NET 使用调色板压缩PNG文件]]></title><description><![CDATA[<p>项目用到截屏，然后把图片存储到数据库中。一开始使用的是C#内置的PNG输出方式（GDI+），截屏分辨率大约是780x340，文件大小约40K，看起来还可以，但时间一长，数据库会变得很大，给存储和备份都造成麻烦。</p>
<p>截屏目的只是为了以后可能用来排除错误，所以清晰度基本没要求，能看到大概就行。试了下JPGE编码，图像质量调到60，文件大小约30K，是小了一些。</p>
<p>突然想起来有个TinyPNG的在线工具，把文件下载下来，扔上去，TinyPNG给压缩出来一个10K多点的文件。不甘心，于是开始搜索起来TinyPNG类似算法的解决方案。</p>
<p>TinyPNG的算法说起来也简单，就是将24bit的图片转为8bit的调色板图片，然后使用抖动算法尽量还原图片信息。这里生成调色板及抖动算法是关键，调色板生成的不好，最终图片杂色、条纹就会明显。</p>
<p>找到一个名为nQuant.NET的库，在nuget上就有，于是安装上进行了一下测试。<br>
库的使用也很简单</p>
<pre><code>var quantizer = new nQuant.WuQuantizer();
using (var quantized = quantizer.QuantizeImage(argbImage, 10, 70))
{
    using</code></pre>]]></description><link>https://zhujinliang.com/csharp-png-image-compress-with-nquantnet/</link><guid isPermaLink="false">60704f30a40233043074d33c</guid><dc:creator><![CDATA[zhujinliang]]></dc:creator><pubDate>Fri, 09 Apr 2021 13:13:27 GMT</pubDate><content:encoded><![CDATA[<p>项目用到截屏，然后把图片存储到数据库中。一开始使用的是C#内置的PNG输出方式（GDI+），截屏分辨率大约是780x340，文件大小约40K，看起来还可以，但时间一长，数据库会变得很大，给存储和备份都造成麻烦。</p>
<p>截屏目的只是为了以后可能用来排除错误，所以清晰度基本没要求，能看到大概就行。试了下JPGE编码，图像质量调到60，文件大小约30K，是小了一些。</p>
<p>突然想起来有个TinyPNG的在线工具，把文件下载下来，扔上去，TinyPNG给压缩出来一个10K多点的文件。不甘心，于是开始搜索起来TinyPNG类似算法的解决方案。</p>
<p>TinyPNG的算法说起来也简单，就是将24bit的图片转为8bit的调色板图片，然后使用抖动算法尽量还原图片信息。这里生成调色板及抖动算法是关键，调色板生成的不好，最终图片杂色、条纹就会明显。</p>
<p>找到一个名为nQuant.NET的库，在nuget上就有，于是安装上进行了一下测试。<br>
库的使用也很简单</p>
<pre><code>var quantizer = new nQuant.WuQuantizer();
using (var quantized = quantizer.QuantizeImage(argbImage, 10, 70))
{
    using (var ms = new MemoryStream())
    {
        quantized.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
        data = ms.ToArray();
    }
}
</code></pre>
<p>注意这个库只接受32bit，也就是ARGB图片作为输入。</p>
<p>经测试同样的图片，经nQuant处理后大小约为13KB，略大于TinyPNG处理的，也很不错了，比原来小了一半多。或许换一个更好的PNG编码器可以进一步减小体积。</p>
<ul>
<li>其它语言：<br>
这有一个C语言版本的 <a href="https://github.com/kornelski/pngquant">https://github.com/kornelski/pngquant</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[stm32 Checklists - USART篇]]></title><description><![CDATA[<h3 id="">基础初始化过程</h3>
<ol>
<li>APB EnableClock, 查找定时器所在APB及分组(一般APB1 GRP1)，开启时钟<pre><code>LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_USARTx)
</code></pre>
</li>
<li>如果有相应的GPIO，初始化GPIO时钟、端口等。</li>
<li>填写 <code>USART_InitStruct</code>。<pre><code>LL_USART_InitTypeDef USART_InitStruct = {0};
USART_InitStruct.BaudRate = 9600;
USART_InitStruct.DataWidth = LL_USART_DATAWIDTH_8B;
USART_InitStruct.StopBits = LL_USART_STOPBITS_1;
USART_InitStruct.Parity = LL_</code></pre></li></ol>]]></description><link>https://zhujinliang.com/stm32-checklists-adcpian/</link><guid isPermaLink="false">5f0b324da40233043074d0ec</guid><dc:creator><![CDATA[zhujinliang]]></dc:creator><pubDate>Sun, 12 Jul 2020 16:26:48 GMT</pubDate><content:encoded><![CDATA[<h3 id="">基础初始化过程</h3>
<ol>
<li>APB EnableClock, 查找定时器所在APB及分组(一般APB1 GRP1)，开启时钟<pre><code>LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_USARTx)
</code></pre>
</li>
<li>如果有相应的GPIO，初始化GPIO时钟、端口等。</li>
<li>填写 <code>USART_InitStruct</code>。<pre><code>LL_USART_InitTypeDef USART_InitStruct = {0};
USART_InitStruct.BaudRate = 9600;
USART_InitStruct.DataWidth = LL_USART_DATAWIDTH_8B;
USART_InitStruct.StopBits = LL_USART_STOPBITS_1;
USART_InitStruct.Parity = LL_USART_PARITY_NONE;
USART_InitStruct.TransferDirection = LL_USART_DIRECTION_TX_RX;
USART_InitStruct.HardwareFlowControl = LL_USART_HWCONTROL_NONE;
USART_InitStruct.OverSampling = LL_USART_OVERSAMOLING_16;
</code></pre>
</li>
<li>根据需要关闭或打开Overrun检测。该检测启用后，硬件将自动判断是否在接受过程中发生了输入进入，从而造成数据丢失。同时启用后，开启RXNE中断同时会开启ORE中断，给中断处理造成麻烦。<pre><code>LL_USART_DisableOverrunDetect(USARTx);
</code></pre>
</li>
<li>设置异步模式。<pre><code>LL_USART_ConfigAsyncMode(USARTx);
</code></pre>
</li>
<li>初始化USART。<pre><code>LL_USART_Init(USARTx, &amp;USART_InitStruct);
</code></pre>
</li>
<li>启动USART。<pre><code>LL_USART_Enable(USARTx);
</code></pre>
</li>
</ol>
<h3 id="">软件发送一个字节</h3>
<ol>
<li>初始化完毕。</li>
<li>等待传输空闲。<pre><code>while (!LL_USART_IsActiveFlag_TXE(USARTx));
</code></pre>
</li>
<li>发送数据。<pre><code>LL_USART_TransmitData8(USARTx, data);
</code></pre>
</li>
</ol>
<h3 id="">软件接收一个字节</h3>
<ol>
<li>初始化完毕。</li>
<li>读取内容。<pre><code>while (LL_USART_IsActiveFlag_RXNE(USARTx))
{
    uint8_t data = LL_USART_ReceiveData8(USARTx);
}
</code></pre>
</li>
</ol>
<h3 id="">中断接收</h3>
<ol>
<li>初始化完毕。</li>
<li>开启 RXNE 中断。<pre><code>LL_USART_EnableIT_RXNE(USARTx);
</code></pre>
</li>
<li>编写中断处理函数。<pre><code>void USARTx_IRQHandler(void)
{
    if (LL_USART_IsActiveFlag_RXNE(USARTx))
    {
        uint8_t data = LL_USART_ReceiveData8(USARTx);
    }
}
</code></pre>
</li>
<li>如果启用了Overrun，检查并清除ORE标志。<pre><code>if (LL_USART_IsActiveFlag_ORE(USARTx))
{
    LL_USART_ClearFlag_ORE(USARTx);
}
</code></pre>
</li>
</ol>
<h3 id="dma">使用 DMA 接收</h3>
<p>使用 DMA 接收可以最大限度避免丢失数据，同时可降低接收时的 CPU 占用。一种常用的通信模式是，发送数据前配置好 DMA，然后发送数据，同时 DMA 处理数据接收，发送完毕后，根据应接收的字节数判断是否已接收完毕。</p>
<ol>
<li>初始化完毕。</li>
<li>查询外设应使用的DMA控制器及通道，配置 DMA。<pre><code>LL_DMA_SetDataTransferDirection(DMAx, LL_DMA_CHANNEL_x, LL_DMA_DIRECTION_PERIPH_TO_MEMORY);
LL_DMA_SetChannelPriorityLevel(DMAx, LL_DMA_CHANNEL_x, LL_DMA_PRIORITY_LOW);
LL_DMA_SetMode(DMAx, LL_DMA_CHANNEL_x, LL_DMA_MODE_NORMAL);
LL_DMA_SetPeriphIncMode(DMAx, LL_DMA_CHANNEL_x, LL_DMA_PERIPH_NOINCRMENT);
LL_DMA_SetMemoryIncMode(DMAx, LL_DMA_CHANNEL_x, LL_DMA_MEMORY_INCREMENT);
LL_DMA_SetPeriphSize(DMAx, LL_DMA_CHANNEL_x, LL_DMA_PDATAALIGN_BYTE);
LL_DMA_SetMemorySize(DMAx, LL_DMA_CHANNEL_x, LL_DMA_MDATAALIGN_BYTE);
</code></pre>
</li>
<li>设置 DMA 地址，传输数据量。<pre><code>LL_DMA_SetPeriphAddress(DMAx, LL_DMA_CHANNEL_x, LL_USART_DMA_GetRegAddr(USARTx, LL_USART_DMA_REG_DATA_RECEIVE));
LL_DMA_SetMemoryAddress(DMAx, LL_DMA_CHANNEL_x, (uint32_t)buffer);
LL_DMA_SetDataLength(DMAx, LL_DMA_CHANNEL_x, bufferSize);
</code></pre>
</li>
<li>启动 DMA 通道。<pre><code>LL_DMA_EnableChannel(DMAx, LL_DMA_CHANNEL_x);
</code></pre>
</li>
<li>清空 USART 接收缓冲。否则可能造成写入内存的第一个字节不是需要的内容。<pre><code>while (LL_USART_IsActiveFlag_RXNE(USARTx))
{
    uint8_t drop = LL_USART_ReceiveData8(USARTx);
}
</code></pre>
</li>
<li>启动 USART 发送到 DMA。<pre><code>LL_USART_EnableDMAReq_RX(USARTx);
</code></pre>
</li>
<li>查询已接收的字节数。<pre><code>static uint16_t __dma_read_size()
{
    return BUFFER_SIZE - LL_DMA_GetDataLength(DMAx, LL_DMA_CHANNEL_x);
}
</code></pre>
</li>
<li>等待接收到需要大小的数据。<pre><code>static uint16_t __dma_wait_read(uint16_t sizeUntil, uint32_t timeout)
{
    uint32_t tick = HAL_GetTick();
    while (__dma_read_size() &lt; sizeUntil)
    {
        if (HAL_GetTick() - tick &gt; timeout)
        {
            return TIMEOUT;
        }
    }
    return SUCCESS;
}
</code></pre>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[stm32 Checklists - ADC篇]]></title><description><![CDATA[<h3 id="">基础初始化过程</h3>
<ol>
<li>APB EnableClock, 查找定时器所在APB及分组(一般APB1 GRP2)，开启时钟<pre><code>LL_APB1_GRP2_EnableClock(LL_APB1_GRP2_PERIPH_TIMx);
</code></pre>
</li>
<li>如果有相应的GPIO，初始化GPIO时钟、端口等。</li>
<li>填写 <code>ADC_InitStruct</code> 及 <code>ADC_REG_InitStruct</code><pre><code>LL_ADC_InitTypeDef ADC_InitStruct = {0};
LL_ADC_REG_InitTypeDef ADC_REG_InitStruct = {0};
ADC_InitStruct.Clock = LL_ADC_CLOCK_ASYNC;
ADC_InitStruct.Resolution</code></pre></li></ol>]]></description><link>https://zhujinliang.com/stm32-checklists-adc/</link><guid isPermaLink="false">5f046ee0a40233043074d08c</guid><dc:creator><![CDATA[zhujinliang]]></dc:creator><pubDate>Tue, 07 Jul 2020 13:15:30 GMT</pubDate><content:encoded><![CDATA[<h3 id="">基础初始化过程</h3>
<ol>
<li>APB EnableClock, 查找定时器所在APB及分组(一般APB1 GRP2)，开启时钟<pre><code>LL_APB1_GRP2_EnableClock(LL_APB1_GRP2_PERIPH_TIMx);
</code></pre>
</li>
<li>如果有相应的GPIO，初始化GPIO时钟、端口等。</li>
<li>填写 <code>ADC_InitStruct</code> 及 <code>ADC_REG_InitStruct</code><pre><code>LL_ADC_InitTypeDef ADC_InitStruct = {0};
LL_ADC_REG_InitTypeDef ADC_REG_InitStruct = {0};
ADC_InitStruct.Clock = LL_ADC_CLOCK_ASYNC;
ADC_InitStruct.Resolution = LL_ADC_RESOLUTION_12B;
ADC_InitStruct.DataAlignment = LL_ADC_DATA_ALIGN_RIGHT;
ADC_InitStruct.LowPowerMode = LL_ADC_LP_MODE_NONE;
ADC_REG_InitStruct.SequencerDiscont = LL_ADC_REG_SEQ_DISCONT_DISABLE;
ADC_REG_InitStruct.ContinuousMode = LL_ADC_REG_CONV_SINGLE;
ADC_REG_InitStruct.DMATransfer = LL_ADC_REG_DMA_TRANSFER_NONE;
ADC_REG_InitStruct.Overrun = LL_ADC_REG_OVR_DATA_OVERWRITTEM;
</code></pre>
</li>
<li>初始化ADC及ADC_REG。<pre><code>LL_ADC_Init(ADCx, &amp;ADC_InitStruct);
LL_ADC_REG_Init(ADCx, &amp;ADC_REG_InitStruct);
</code></pre>
</li>
<li>其它ADC设置。<pre><code>LL_ADC_REG_SetSequencerScanDirection(ADCx, LL_ADC_REG_SEQ_SCAN_DIR_FORWARD);
LL_ADC_SetSamplingTimeCommonChannels(ADCx, LL_ADC_SAMPLINGTIME_1CYCLE_5);
</code></pre>
</li>
</ol>
<h3 id="">软件触发一次转换并等待转换完毕</h3>
<ol>
<li>ADC已完成基础初始化。</li>
<li>启动校准，并等待校准完成。<pre><code>LL_mDelay(1);
LL_ADC_StartCalibration(ADCx);
while (LL_ADC_IsCalibrationOnGoing(ADCx));
</code></pre>
</li>
<li>启用ADC，并等待ADC启动完毕。<pre><code>LL_ADC_Enable(ADCx);
while (LL_ADC_IsActiveFlag_ADRDY(ADCx) == 0);
</code></pre>
</li>
<li>清除EOC标志，确保转换被执行。<pre><code>if (LL_ADC_IsActiveFlag_EOC(ADCx)) LL_ADC_ClearFlag_EOC(ADCx);
</code></pre>
</li>
<li>设置转换通道。<pre><code>LL_ADC_REG_SetSequencerChannels(ADCx, LL_ADC_CHANNEL_0);
</code></pre>
</li>
<li>启动转换。<pre><code>LL_ADC_StartConversion(ADCx);
</code></pre>
</li>
<li>检查EOC标志。<pre><code>while (LL_ADC_IsActiveFlag_EOC(ADCx) == 0);
LL_ADC_ClearFlag_EOC(ADCx);
</code></pre>
</li>
<li>读取转换结果。<pre><code>LL_ADC_REG_ReadConversionData12(ADCx);
</code></pre>
</li>
</ol>
<h3 id="eoc">使用EOC中断</h3>
<ol>
<li>ADC已完成基础初始化。</li>
<li>启用ADC，并等待ADC启动完毕。<pre><code>LL_ADC_Enable(ADCx);
while (LL_ADC_IsActiveFlag_ADRDY(ADCx) == 0);
</code></pre>
</li>
<li>清除EOC标志，确保转换被执行。<pre><code>if (LL_ADC_IsActiveFlag_EOC(ADCx)) LL_ADC_ClearFlag_EOC(ADCx);
</code></pre>
</li>
<li>设置转换通道。<pre><code>LL_ADC_REG_SetSequencerChannels(ADCx, LL_ADC_CHANNEL_0);
</code></pre>
</li>
<li>启用EOC中断。<pre><code>LL_ADC_EnableIT_EOC(ADCx);
</code></pre>
</li>
<li>设置NVIC。<pre><code>NVIC_SetPriority(ADCx_IRQn, 0);
NVIC_EnableIRQ(ADCx_IRQn);
</code></pre>
</li>
<li>启动转换。<pre><code>LL_ADC_StartConversion(ADCx);
</code></pre>
</li>
<li>在中断服务函数中读取转换结果。<pre><code>void ADCx_IRQHandler(void)
{
    LL_ADC_REG_ReadConversionData12(ADCx);
}
</code></pre>
</li>
</ol>
<h3 id="">使用触发源</h3>
<p>参考时钟配置部分 <a href="https://zhujinliang.com/stm32-checklist-tim/">https://zhujinliang.com/stm32-checklist-tim/</a></p>
<ol>
<li>ADC已完成基础初始化。</li>
<li>在<code>ADC_REG_InitStruct</code>中指定触发源。<pre><code>ADC_REG_InitStruct.TriggerSource = LL_ADC_REG_TRIG_EXT_TIMx_TRGO;
</code></pre>
</li>
<li>设置<code>TriggerEdge</code>。<pre><code>LL_ADC_REG_SetTriggerEdge(ADCx, LL_ADC_REG_TRIG_EXT_RISING);
</code></pre>
</li>
<li>启用ADC。<pre><code>LL_ADC_Enable(ADCx);
</code></pre>
</li>
<li>设置转换通道。<pre><code>LL_ADC_REG_SetSequencerChannels(ADCx, LL_ADC_CHANNEL_0);
</code></pre>
</li>
<li>启用EOC中断。<pre><code>LL_ADC_EnableIT_EOC(ADCx);
</code></pre>
</li>
<li>设置NVIC。<pre><code>NVIC_SetPriority(ADCx_IRQn, 0);
NVIC_EnableIRQ(ADCx_IRQn);
</code></pre>
</li>
<li>启动转换。<pre><code>LL_ADC_StartConversion(ADCx);
</code></pre>
</li>
<li>在中断服务函数中读取转换结果。<pre><code>void ADC_IRQHandler(void)
{
    LL_ADC_REG_ReadConversionData12(ADCx);
}
</code></pre>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[stm32 Checklists - TIM篇]]></title><description><![CDATA[<h3 id="">基础初始化过程</h3>
<ol>
<li>APB EnableClock, 查找定时器所在APB及分组(一般APB1 GRP1)，开启时钟<pre><code>LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIMx);
</code></pre>
</li>
<li>填写 TIM_InitStruct<pre><code>LL_TIM_InitTypeDef TIM_InitStruct = {0};
TIM_InitStruct.Prescaler = 71;
TIM_InitStruct.CounterMode = LL_TIM_COUNTERMODE_UP;
TIM_InitStruct.Autoreload = 1000;
TIM_InitStruct.ClockDivision = LL_TIM_CLOCKDIVIDION_DIV1;
</code></pre>
</li>
<li>初始化TIM<pre><code>LL_TIM_</code></pre></li></ol>]]></description><link>https://zhujinliang.com/stm32-checklist-tim/</link><guid isPermaLink="false">5f0468dea40233043074d041</guid><dc:creator><![CDATA[zhujinliang]]></dc:creator><pubDate>Tue, 07 Jul 2020 13:15:03 GMT</pubDate><content:encoded><![CDATA[<h3 id="">基础初始化过程</h3>
<ol>
<li>APB EnableClock, 查找定时器所在APB及分组(一般APB1 GRP1)，开启时钟<pre><code>LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIMx);
</code></pre>
</li>
<li>填写 TIM_InitStruct<pre><code>LL_TIM_InitTypeDef TIM_InitStruct = {0};
TIM_InitStruct.Prescaler = 71;
TIM_InitStruct.CounterMode = LL_TIM_COUNTERMODE_UP;
TIM_InitStruct.Autoreload = 1000;
TIM_InitStruct.ClockDivision = LL_TIM_CLOCKDIVIDION_DIV1;
</code></pre>
</li>
<li>初始化TIM<pre><code>LL_TIM_Init(TIMx, &amp;TIM_InitStruct);
</code></pre>
</li>
<li>设置是否启用 ARRPreload，其作用类似影子寄存器（可选）。<pre><code>LL_TIM_DisableARRPreload(TIMx);
</code></pre>
</li>
<li>设置时钟源（可选）。<pre><code>LL_TIM_SetClockSource(TIMx, LL_TIM_CLOCKSOURCE_INTERNAL);
</code></pre>
</li>
</ol>
<h3 id="update">产生Update中断</h3>
<p>用于定时产生一个固定时间间隔的中断</p>
<ol>
<li>基础初始化。</li>
<li>启用Update中断。<pre><code>LL_TIM_EnableIT_UPDATE(TIMx);
</code></pre>
</li>
<li>设置NVIC。<pre><code>NVIC_SetPriority(TIMx_IRQn, 0);
NVIC_EnableIRQ(TIMx_IRQn);
</code></pre>
</li>
<li>启用时钟。初始化完毕。<pre><code>LL_TIM_EnableCounter(TIMx);
</code></pre>
</li>
<li>编写IRQHandler函数。在<code>stm32fxxx_it.c</code>文件中编写代码，注意必须有清除标志的动作，否则会导致一直进入中断。<pre><code>void TIMx_IRQHandler(void)
{
    if (LL_TIM_IsActiveFlag_UPDATE(TIMx))
    {
        LL_TIM_ClearFlag_UPDATE(TIMx);
    }
}
</code></pre>
</li>
<li>检查启动文件中是否有相应的入口。<pre><code>DCD  TIMx_IRQHandler  ; TIMx
</code></pre>
</li>
</ol>
<h3 id="adc">作为ADC的触发源</h3>
<p>这里使用UPDATE事件，同理可用CC一类事件。</p>
<ol>
<li>基础初始化。</li>
<li>设置触发输出。<pre><code>LL_TIM_SetTriggerOutput(TIMx, LL_TIM_TRGO_UPDATE);
</code></pre>
</li>
<li>启用时钟。<pre><code>LL_TIM_EnableCounter(TIMx);
</code></pre>
</li>
<li>启用触发事件。<pre><code>LL_TIM_EnableUpdateEvent(TIMx);
</code></pre>
</li>
<li>ADC中设置TriggerSource。<pre><code>ADC_REG_InitStruct.TriggerSource = LL_ADC_REG_TRIG_EXT_TIMx_TRGO;
</code></pre>
</li>
<li>ADC应启用且开启转换。否则不会进行转换及产生EOC中断。<pre><code>LL_ADC_Enable(ADCx);
LL_ADC_REG_StartConversion(ADCx);
</code></pre>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[使用命令行生成DWG文件的预览图片]]></title><description><![CDATA[<p>最近做的一个系统，需要有文件预览功能，不巧客户有大量AutoCAD的文件，于是研究了一下DWG文件生成预览图片的方法。</p>
<ol start="0">
<li>
<p>需要 Windows 环境</p>
</li>
<li>
<p>下载安装DWGTrueView软件：<a href="https://www.autodesk.com.cn/products/dwg">下载地址</a></p>
</li>
<li>
<p>打开一个DWG文件，对导出选项一些设置。可以用一个<a href="https://forums.autodesk.com/autodesk/attachments/autodesk/109/21306/1/GVR-%20Residential.dwg">老外提供的文件</a>，作为示例。</p>
</li>
<li>
<p>按Ctrl+P或菜单里的 Output-&gt;Plot 命令，打开 Plot 设置窗口。这里我们主要设置 plotter 导出的图片大小参数。<br>
<img src="https://zhujinliang.com/content/images/2020/05/1589526140-1-.png" alt="1589526140-1-"><br>
Printer/plotter里面，Name选择PublishToWeb PNG.pc3，然后点击Properties...按钮。<br>
<img src="https://zhujinliang.com/content/images/2020/05/1589526226-1-.png" alt="1589526226-1-"><br>
在Device and Document Settings选项卡中，找到Custom Paper Sizes，然后点击下方的Add...。<br>
<img src="https://zhujinliang.com/content/images/2020/05/1589526266-1-.png" alt="1589526266-1-"><br>
在添加向导中，选择Start from scratch，进入下一步。<br>
<img src="https://zhujinliang.com/content/images/2020/05/1589526298-1-.png" alt="1589526298-1-"><br>
填入需要的图片尺寸大小，单位为像素。<br>
<img src="https://zhujinliang.com/content/images/2020/05/1589526390-1-.png" alt="1589526390-1-"><br>
取个名字。下一步完成即可。</p></li></ol>]]></description><link>https://zhujinliang.com/shi-yong-ming-ling-zhuan-huan-dwgwen-jian/</link><guid isPermaLink="false">5ebe3217a40233043074cf97</guid><dc:creator><![CDATA[zhujinliang]]></dc:creator><pubDate>Fri, 15 May 2020 07:54:23 GMT</pubDate><content:encoded><![CDATA[<p>最近做的一个系统，需要有文件预览功能，不巧客户有大量AutoCAD的文件，于是研究了一下DWG文件生成预览图片的方法。</p>
<ol start="0">
<li>
<p>需要 Windows 环境</p>
</li>
<li>
<p>下载安装DWGTrueView软件：<a href="https://www.autodesk.com.cn/products/dwg">下载地址</a></p>
</li>
<li>
<p>打开一个DWG文件，对导出选项一些设置。可以用一个<a href="https://forums.autodesk.com/autodesk/attachments/autodesk/109/21306/1/GVR-%20Residential.dwg">老外提供的文件</a>，作为示例。</p>
</li>
<li>
<p>按Ctrl+P或菜单里的 Output-&gt;Plot 命令，打开 Plot 设置窗口。这里我们主要设置 plotter 导出的图片大小参数。<br>
<img src="https://zhujinliang.com/content/images/2020/05/1589526140-1-.png" alt="1589526140-1-"><br>
Printer/plotter里面，Name选择PublishToWeb PNG.pc3，然后点击Properties...按钮。<br>
<img src="https://zhujinliang.com/content/images/2020/05/1589526226-1-.png" alt="1589526226-1-"><br>
在Device and Document Settings选项卡中，找到Custom Paper Sizes，然后点击下方的Add...。<br>
<img src="https://zhujinliang.com/content/images/2020/05/1589526266-1-.png" alt="1589526266-1-"><br>
在添加向导中，选择Start from scratch，进入下一步。<br>
<img src="https://zhujinliang.com/content/images/2020/05/1589526298-1-.png" alt="1589526298-1-"><br>
填入需要的图片尺寸大小，单位为像素。<br>
<img src="https://zhujinliang.com/content/images/2020/05/1589526390-1-.png" alt="1589526390-1-"><br>
取个名字。下一步完成即可。回到刚才的窗口，可以看到刚刚添加的纸张大小。</p>
<p>这有一份常见像素数量与尺寸的表格，可以作为对照。<br>
<img src="https://zhujinliang.com/content/images/2020/05/a50f4bfbfbedab64c77b5bb6fa36afc379311ebe.png" alt="a50f4bfbfbedab64c77b5bb6fa36afc379311ebe"></p>
</li>
<li>
<p>回到刚才的 Plot 设置窗口，如同所示，Paper size 选择刚刚添加的尺寸，Plot area 选择 Extents，Plot scale 选择 Fit to paper，Plot style table 选择 dwgviewer.ctb，默认的 monochrome.ctb 会使导出的文件为黑白的，其它默认。此时可点击 OK 按钮，导出一个文件测试一下。</p>
</li>
<li>
<p>当然我们需要的是能够自动完成导出，在软件安装目录（C:\Program Files\Autodesk\DWG TrueView 2021 - English）下面可以找到 accoreconsole.exe 程序，它可以执行批处理，完成导出工作。这个命令需要输入一个 DWG 文件，和一个脚本文件。参考https://blog.csdn.net/autodeskinventorapi/article/details/45075453</p>
</li>
<li>
<p>上面给的脚本可能只适用于完整的 AutoCAD，其中有些命令无法在 DWG TrueView 中执行，需要修改一下。同时我这里改成了导出 PNG 图像文件。如需要导出 PDF 文件，可仿照修改。由于某些命令无法执行，导出文件的文件名固定为 <code>C:\Downloads\test.png</code>，实际应用时可通过模板生成该脚本文件，然后再执行accoreconsole.exe。</p>
</li>
</ol>
<pre><code>;Command:
_plot
;Detailed plot configuration? [Yes/No] &lt;No&gt;: 
Yes
;Enter a layout name or [?] &lt;Model&gt;:
Model
;Enter an output device name or [?] &lt;None&gt;:
PublishToWeb PNG.pc3
;Enter paper size or [?] &lt;ANSI A (11.00 x 8.50 Inches)&gt;:
800W
;Enter drawing orientation [Portrait/Landscape] &lt;Portrait&gt;: 
Landscape
;Plot upside down? [Yes/No] &lt;No&gt;:
No
;Enter plot area [Display/Extents/Limits/View/Window] &lt;Display&gt;: 
Extents
;Enter plot scale (Plotted Inches=Drawing Units) or [Fit] &lt;Fit&gt;:
Fit
;Enter plot offset (x,y) or [Center] &lt;0.00,0.00&gt;:
Center
;Plot with plot styles? [Yes/No] &lt;Yes&gt;:
Yes
;Enter plot style table name or [?] (enter . for none) &lt;&gt;:
dwgviewr.ctb
;Plot with lineweights? [Yes/No] &lt;Yes&gt;:
Yes
;Enter shade plot setting [As displayed/legacy Wireframe/legacy Hidden/Visualstyles/Rendered] &lt;As displayed&gt;:

;Enter file name &lt;C:\Work\solids-Model.pdf&gt;:
C:\Downloads\test.png
;Save changes to page setup? Or set shade plot quality? [Yes/No/Quality] &lt;N&gt;:
No
;Proceed with plot [Yes/No] &lt;Y&gt;:
Yes

</code></pre>
<p>其中的<code>800W</code>为刚创建的纸张大小的名称。输出文件的目录一定要存在，否则会导致脚本执行错误。最后要多加一个空的换行。修改 output device 后，后面的参数表可能会有不同，需要运行一下，然后根据返回的结果进行修改。</p>
<ol start="7">
<li>编写一个 bat 文件进项测试。</li>
</ol>
<pre><code>cd &quot;C:\Program Files\Autodesk\DWG TrueView 2021 - English&quot;
c:
cls
accoreconsole.exe /i &quot;C:\Downloads\GVR- Residential.dwg&quot; /s &quot;C:\Downloads\test.scr&quot; /l en-US
PAUSE
</code></pre>
<p>没问题的话，导出工作完成后，会在对应的目录产生一个 test.png 文件。<br>
<img src="https://zhujinliang.com/content/images/2020/05/test-1.png" alt="test-1"></p>
]]></content:encoded></item><item><title><![CDATA[C# WinForm 如何保存窗口的状态]]></title><description><![CDATA[<p>软件中通常会有一种保存窗口状态的功能，即打开程序时，窗口出现在上次关闭的位置，窗口大小与关闭前一样。</p>
<p>实现这个功能需要保存三个变量：位置、大小、以及窗口状态。</p>
<pre><code>public class FormState
{
    public Point Location;
    public Size Size;
    public FormWindowState WindowState;
}
</code></pre>
<p>位置大小不必多说，窗口状态表示窗口处于正常状态、最大化、还是最小化。实际使用中，我们不希望用户打开程序后，窗口处于最小化状态，所以只需要保存是否处于最大化状态即可。</p>
<p>如果我们在窗口关闭时，直接保存窗口的 Location 和 Size 属性，看似成功了，但会带来一个问题：如果我们关闭窗口时，窗口正处于最大化状态，下次打开窗口后，窗口从最大化恢复正常时，尺寸并不会缩小到原来的状态。这是因为我们没有保存窗口恢复正常时应恢复的尺寸，这时应该保存 RestoreBounds 变量。</p>
<p>代码如下:</p>
<pre><code>private void SaveFormState()
{
    if</code></pre>]]></description><link>https://zhujinliang.com/c-winform-bao-cun-chuang-kou-zhuang-tai-de-fang-fa/</link><guid isPermaLink="false">5e7ad0aaa40233043074cf60</guid><dc:creator><![CDATA[zhujinliang]]></dc:creator><pubDate>Wed, 25 Mar 2020 03:48:01 GMT</pubDate><content:encoded><![CDATA[<p>软件中通常会有一种保存窗口状态的功能，即打开程序时，窗口出现在上次关闭的位置，窗口大小与关闭前一样。</p>
<p>实现这个功能需要保存三个变量：位置、大小、以及窗口状态。</p>
<pre><code>public class FormState
{
    public Point Location;
    public Size Size;
    public FormWindowState WindowState;
}
</code></pre>
<p>位置大小不必多说，窗口状态表示窗口处于正常状态、最大化、还是最小化。实际使用中，我们不希望用户打开程序后，窗口处于最小化状态，所以只需要保存是否处于最大化状态即可。</p>
<p>如果我们在窗口关闭时，直接保存窗口的 Location 和 Size 属性，看似成功了，但会带来一个问题：如果我们关闭窗口时，窗口正处于最大化状态，下次打开窗口后，窗口从最大化恢复正常时，尺寸并不会缩小到原来的状态。这是因为我们没有保存窗口恢复正常时应恢复的尺寸，这时应该保存 RestoreBounds 变量。</p>
<p>代码如下:</p>
<pre><code>private void SaveFormState()
{
    if (WindowState == FormWindowState.Maximized)
    {
        formState.Location = RestoreBounds.Location;
        formState.Size = RestoreBounds.Size;
        formState.WindowState = FormWindowState.Maximized;
    }
    else if (WindowState == FormWindowState.Normal)
    {
        formState.Location = Location;
        formState.Size = Size;
        formState.WindowState = FormWindowState.Normal;
    }
    else
    {
        formState.Location = RestoreBounds.Location;
        formState.Size = RestoreBounds.Size;
        formState.WindowState = FormWindowState.Normal;
    }
    formState.Save()
}
</code></pre>
]]></content:encoded></item><item><title><![CDATA[常用SOT23封装DC-DC变换器]]></title><description><![CDATA[<pre><code>Buck / Step Down

IC        Vref(V)  Iout(mA)  feq(MHZ)  Vin(min~max)  Vout(max)  Effi...  MaxDute  Pinout
PAM2312   0.6      1000      1.5                                                       ----
TD6811    0.6      1200      1.5                                                   EN -|  |- FB
NCP1529   0.6      1000      1.7       2.5 ~ 5.5                96%      100%     GND -|  |
RT8008</code></pre>]]></description><link>https://zhujinliang.com/chang-yong-sot23feng-zhuang-dc-dc/</link><guid isPermaLink="false">5d75b29ba40233043074cd8d</guid><dc:creator><![CDATA[zhujinliang]]></dc:creator><pubDate>Mon, 09 Sep 2019 02:02:59 GMT</pubDate><content:encoded><![CDATA[<pre><code>Buck / Step Down

IC        Vref(V)  Iout(mA)  feq(MHZ)  Vin(min~max)  Vout(max)  Effi...  MaxDute  Pinout
PAM2312   0.6      1000      1.5                                                       ----
TD6811    0.6      1200      1.5                                                   EN -|  |- FB
NCP1529   0.6      1000      1.7       2.5 ~ 5.5                96%      100%     GND -|  |
RT8008    0.5      600       1.5       2.5 ~ 5.5                95%      100%      SW -|  |- VIN
                                                                                       ----

XRP6681   0.6      1000      1.5                                                       ----
TPS62200  0.5      300       1.5       2.5 ~ 6                  95%      100%     VIN -|  |- SW
LM3674    0.5      600       2         2.7 ~ 5.5                                  GND -|  |
RT8009    0.5      600       1.25      2.5 ~ 5.5                95%      100%      EN -|  |- FB
                                                                                       ----

--------

Boost / Step Up

IC        Vref(V)  Iout(mA)  feq(MHZ)  Vin(min~max)  Vout(max)  Effi...  MaxDute  Pinout
TPS61040  1.233    400       1         1.8 ~ 6       28                                ----
TPS61041  1.233    200       1         1.8 ~ 6       28                            SW -|  |- VIN 
MP1540    1.25     200       1.3       2.5 ~ 6       18                           GND -|  |
                                                                                   FB -|  |- EN
                                                                                       ----

SX1308    0.6      2000      1.2       2   ~ 24      28         97%      90%           ----
MT3608    0.6      2000      1.2       2   ~ 24      28         97%      90%       SW -|  |- NC
                                                                                  GND -|  |- VIN
                                                                                   FB -|  |- EN
                                                                                       ----
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Golang TLS 安全链接通信]]></title><description><![CDATA[<h3 id="">场景说明</h3>
<ul>
<li>通信双方使用自签名证书进行安全通信。</li>
<li>启用双向验证。</li>
<li>使用 ECC 证书。ECC 证书在建立握手时的通信数据量、链接建立速度、安全性相比 RSA 都有优势。</li>
</ul>
<h3 id="">制作证书</h3>
<p>参见： <a href="https://zhujinliang.com/create-ecc-cert-with-openssl/">使用 OpenSSL 制作自签名 ECC 证书</a></p>
<h3 id="tlsconfig">tls.Config 配置要点</h3>
<ul>
<li><code>Certificates</code> 证书，对于服务器端就是服务器的证书，对于客户端就是客户的证书。</li>
<li><code>ClientAuth</code> 客户端验证策略，这里我们选择 <code>RequireAndVerifyClientCert</code>，即要求客户端发送证书并验证其真实性。</li>
<li><code>ClientCAs</code> 签名使用的根证书，服务器端使用，用于验证客户端的真实性（如果开启了客户端验证）。</li>
<li><code>RootCAs</code> 签名使用的根证书，客户端使用，用于验证服务器的真实性。</li>
<li><code>ServerName</code> 服务器端证书对应的 <code>CommonName</code>，用于验证服务器的真实性。</li>
<li><code>CipherSuites</code> 用于协商加密策略，由于我们使用 ECC 证书，所以只选择</li></ul>]]></description><link>https://zhujinliang.com/shi-yong-golang-tls-an-quan-lian-jie/</link><guid isPermaLink="false">5c1a1e12c05b425f3be7fc4c</guid><dc:creator><![CDATA[zhujinliang]]></dc:creator><pubDate>Tue, 30 Jul 2019 03:54:13 GMT</pubDate><content:encoded><![CDATA[<h3 id="">场景说明</h3>
<ul>
<li>通信双方使用自签名证书进行安全通信。</li>
<li>启用双向验证。</li>
<li>使用 ECC 证书。ECC 证书在建立握手时的通信数据量、链接建立速度、安全性相比 RSA 都有优势。</li>
</ul>
<h3 id="">制作证书</h3>
<p>参见： <a href="https://zhujinliang.com/create-ecc-cert-with-openssl/">使用 OpenSSL 制作自签名 ECC 证书</a></p>
<h3 id="tlsconfig">tls.Config 配置要点</h3>
<ul>
<li><code>Certificates</code> 证书，对于服务器端就是服务器的证书，对于客户端就是客户的证书。</li>
<li><code>ClientAuth</code> 客户端验证策略，这里我们选择 <code>RequireAndVerifyClientCert</code>，即要求客户端发送证书并验证其真实性。</li>
<li><code>ClientCAs</code> 签名使用的根证书，服务器端使用，用于验证客户端的真实性（如果开启了客户端验证）。</li>
<li><code>RootCAs</code> 签名使用的根证书，客户端使用，用于验证服务器的真实性。</li>
<li><code>ServerName</code> 服务器端证书对应的 <code>CommonName</code>，用于验证服务器的真实性。</li>
<li><code>CipherSuites</code> 用于协商加密策略，由于我们使用 ECC 证书，所以只选择 <code>ECDHE_ECDSA</code> 系列，排除掉安全性较差的 <code>AES_128_CBC_SHA</code> 方式，最终选择 <code>ECDHE_ECDSA_WITH_AES_128_GCM_SHA256</code> <code>ECDHE_ECDSA_WITH_AES_256_GCM_SHA384</code> <code>ECDHE_ECDSA_WITH_CHACHA20_POLY1305</code> 这三种方式。</li>
<li><code>MinVersion</code> 要求最小 TLS 版本号，计划通信双方都使用 Golang 开发，不存在兼容性问题，<code>MinVerson</code> 直接开到最高，即 <code>VersionTLS12</code>。</li>
</ul>
<h3 id="">主要流程</h3>
<ul>
<li>
<p>服务器端</p>
<ol>
<li>载入根证书，用于验证客户端的真实性。Golang 没有提供便捷的载入自签名根证书的方法，需要自行通过 <code>x509.NewCertPool()</code> 去创建 CertPool 然后添加证书。</li>
<li>载入服务器证书，用于发送给客户端以验证服务器真实性。Golang 提供了 <code>tls.LoadX509KeyPair</code> 函数去加载证书和私钥对。</li>
<li>配置 <code>tls.Config</code></li>
<li>使用 <code>Listen</code> 监听端口，之后接受连接，收发数据与一般 TCP 链接无异</li>
</ol>
</li>
<li>
<p>客户端</p>
<ol>
<li>载入根证书，用于验证服务器端的真实性。</li>
<li>载入客户证书，用于发送给服务器端以验证客户的真实性。</li>
<li>配置 <code>tls.Config</code></li>
<li>使用 <code>Dial</code> 请求连接，之后收发数据与一般 TCP 链接无异</li>
</ol>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[VERTIV GXE UPS 通信协议]]></title><description><![CDATA[<p>UPS机器随机附送的软件名称为 SiteManager，只能运行于 Windows 主机上。本文协议内容非官方，是通过串口抓包，以及编写软件模拟串口响应来观察 SiteManager 软件反应整理得出的。</p>
<h3 id="">连接方式</h3>
<ol>
<li>
<p>USB-B 端口连接</p>
<p>此种方式连接后，PC端识别为 USB-ACM 设备，可映射为串口或TTY设备，数据收发方式与串口连接无异。</p>
</li>
<li>
<p>串口连接(RS232)</p>
<p>串口波特率为 9600，8 数据位 1 停止位，无奇偶校验</p>
</li>
</ol>
<h3 id="">请求响应帧</h3>
<p>基本格式：数据以ASCII码字符形式收发，以<code>~</code>符号开始，换行符<code>\d</code>结束，内容由<code>0~9 A~F</code>以及空格组成。</p>
<p>请求格式: <code>aabbccddee...ffff</code></p>
<ul>
<li><code>aa</code>: 地址 <code>10</code> 表示广播</li>
<li><code>bb</code></li></ul>]]></description><link>https://zhujinliang.com/vertiv-gxe-ups-protocol/</link><guid isPermaLink="false">5d3aa8f2a40233043074cd70</guid><dc:creator><![CDATA[zhujinliang]]></dc:creator><pubDate>Fri, 26 Jul 2019 07:18:31 GMT</pubDate><content:encoded><![CDATA[<p>UPS机器随机附送的软件名称为 SiteManager，只能运行于 Windows 主机上。本文协议内容非官方，是通过串口抓包，以及编写软件模拟串口响应来观察 SiteManager 软件反应整理得出的。</p>
<h3 id="">连接方式</h3>
<ol>
<li>
<p>USB-B 端口连接</p>
<p>此种方式连接后，PC端识别为 USB-ACM 设备，可映射为串口或TTY设备，数据收发方式与串口连接无异。</p>
</li>
<li>
<p>串口连接(RS232)</p>
<p>串口波特率为 9600，8 数据位 1 停止位，无奇偶校验</p>
</li>
</ol>
<h3 id="">请求响应帧</h3>
<p>基本格式：数据以ASCII码字符形式收发，以<code>~</code>符号开始，换行符<code>\d</code>结束，内容由<code>0~9 A~F</code>以及空格组成。</p>
<p>请求格式: <code>aabbccddee...ffff</code></p>
<ul>
<li><code>aa</code>: 地址 <code>10</code> 表示广播</li>
<li><code>bb</code>: <code>00</code> 表示广播 <code>01</code> 表示请求地址</li>
<li><code>cc</code>: 值为 <code>2A</code> 作用不详</li>
<li><code>dd</code>: 命令</li>
<li><code>ee</code>: 参数, 最少4字节</li>
<li><code>ffff</code>: 校验码</li>
</ul>
<p>响应格式: <code>aabbccddeeffgg...hhhh</code></p>
<ul>
<li><code>aa</code>: 地址</li>
<li><code>bb</code>: 值为 <code>01</code> 作用不详</li>
<li><code>cc</code>: 值为 <code>2A</code> 作用不详</li>
<li><code>dd</code>: 响应错误码 <code>00</code> 正常 <code>01</code> 地址错误 <code>02</code> 校验码错误</li>
<li><code>ee</code>: 数据标签</li>
<li><code>ff</code>: 后续数据长度</li>
<li><code>gg</code>: 数据，如ee为0则没有该项</li>
<li><code>hhhh</code>: 校验码</li>
</ul>
<h3 id="">校验码</h3>
<p>校验码计算方法：从起始字符之后，每个字符的 ASCII 码值（即二进制数值）相加，直到数据结束，然后用 0x10000 减去相加之和，结果表示为 4 位十六进制文本即可。</p>
<p>文章最后有校验码计算工具。</p>
<h3 id="">请求命令</h3>
<p>注：后续表示的数字，如未特殊说明，均为16进制</p>
<ul>
<li><code>50</code> 查找设备</li>
<li><code>42</code> 读取模拟量</li>
<li><code>43</code> 读取逻辑量（枚举类型）</li>
<li><code>44</code> 读取逻辑量</li>
<li><code>45</code> 遥控命令</li>
<li><code>47</code> 读取EEPROM配置</li>
<li><code>49</code> 遥调命令</li>
<li><code>E6</code> 不详</li>
</ul>
<h4 id="50">50 查找设备</h4>
<ul>
<li>参数 <code>0000</code></li>
<li>响应数据类型为 <code>0000</code>，长度为 <code>00</code></li>
</ul>
<h4 id="42">42 读取模拟量</h4>
<ul>
<li>参数 <code>0000</code></li>
<li>响应数据类型为 <code>0050</code>，长度为 <code>56</code></li>
</ul>
<h4 id="43">43 读取逻辑量（枚举类型）</h4>
<ul>
<li>参数 <code>0000</code></li>
<li>响应数据类型为 <code>00B0</code>，长度为 <code>14</code></li>
</ul>
<h4 id="44">44 读取逻辑量</h4>
<ul>
<li>参数 <code>0000</code></li>
<li>响应数据类型为 <code>0070</code>，长度为 <code>36</code></li>
</ul>
<h4 id="45">45 遥控命令</h4>
<ul>
<li>
<p>参数 aaaabbbb</p>
</li>
<li>
<p>aaaa: 值为 C004 作用不详</p>
</li>
<li>
<p>bbbb: 命令</p>
<p><code>1001</code>: 开始电池有无自检<br>
<code>1002</code>: 开始电池维护自检<br>
<code>1003</code>: 结束电池自检<br>
<code>2001</code>: UPS开机<br>
<code>2002</code>: UPS关机<br>
<code>2003</code>: UPS关闭</p>
</li>
</ul>
<h4 id="49">49 遥调命令</h4>
<ul>
<li>
<p>参数 aaaabbcccc</p>
</li>
<li>
<p>aaaa: 值为 A006 作用不详</p>
</li>
<li>
<p>bb: 地址</p>
</li>
<li>
<p>cccc: 值</p>
<p><code>EF</code>: 电池自检周期     <code>0000</code>:禁止周期自检 <code>0001</code>:3个月 <code>0002</code>:6个月 <code>0003</code>:9个月 <code>0004</code>:12个月<br>
<code>F0</code>: ECO模式         <code>0001</code>:禁止 <code>0002</code>:允许<br>
<code>F1</code>: 电池EOD自动开机  <code>0001</code>:允许 <code>0002</code>:禁止<br>
<code>F2</code>: 设备地址        <code>00xx</code>:xx=1～99 (测试无效果)</p>
</li>
</ul>
<h3 id="">读取模拟量</h3>
<p>举例设备返回下列字符串（不含起始及结尾符号）：</p>
<p><code>21012A005056005960 5630 0000 0FF5138A 081387596000000000000004D8138A017DEE61</code></p>
<p>其中 <code>21</code> 为设备地址，<code>50</code> 为数据标签，<code>56</code> 为数据长度，最后 4 字节 <code>EE61</code> 为校验码。</p>
<p>数据长度后面填充 2 字节 <code>00</code>，之后为第一个模拟量数值。空格符号填充区域可能为该设备不支持的数据字段。</p>
<p>例中第一个值为 <code>5960</code>，表示输入电压，将其转换为 10 进制后为 22880，即输入电压为 228.80V。后续字段数值，除电池后备时间外，均为上述格式，2 位小数精度。电池后备时间精确到分钟。</p>
<p>各字段含义及单位如下：</p>
<ol>
<li>输入电压 V</li>
<li>空白</li>
<li>空白</li>
<li>输出电压 V</li>
<li>空白</li>
<li>空白</li>
<li>输出电流 A</li>
<li>空白</li>
<li>空白</li>
<li>电池电压 V</li>
<li>输出频率 Hz</li>
<li>空白</li>
<li>空白</li>
<li>输入频率 Hz</li>
<li>旁路电压 V</li>
<li>旁路电流 A</li>
<li>有功功率 KW</li>
<li>视在功率 KVA</li>
<li>负载率 %</li>
<li>旁路频率 Hz</li>
<li>电池后备时间 Min</li>
</ol>
<h3 id="">读取逻辑量</h3>
<p>逻辑量分枚举类型和逻辑类型两种。枚举类型对于一个字段，可能有多种状态。</p>
<p>举例请求 <code>43</code> 命令后，设备返回下列字符串（不含起始及结尾符号）：</p>
<p><code>21012A00B014000107E1E1E1E0E0 E1</code></p>
<p>设备地址、数据类型、数据长度、校验码部分不再赘述。数据中每个字节（连续两个字符）表示一个逻辑量。每个逻辑量值似乎只有低4位有效。</p>
<p>各字段含义如下：</p>
<ol>
<li>
<p>未知，值为 <code>00</code></p>
</li>
<li>
<p>状态流动图</p>
<ul>
<li>0: 不供电，电池充电</li>
<li>1: 市电逆变供电，电池充电</li>
<li>2: 旁路供电，电池充电</li>
<li>3: 旁路供电，电池充电</li>
<li>4及以上: 不供电，电池不充电</li>
</ul>
</li>
<li>
<p>不详，值为 07</p>
</li>
<li>
<p>供电方式</p>
<ul>
<li>1: 市电逆变供电</li>
<li>2: 电池逆变供电</li>
</ul>
</li>
<li>
<p>电池充放电</p>
<ul>
<li>0: 非充非放</li>
<li>1: 浮充</li>
<li>2: 均充</li>
<li>3: 放电</li>
</ul>
</li>
<li>
<p>电池自检</p>
<ul>
<li>0: 自检中</li>
<li>1: 不在自检</li>
</ul>
</li>
<li>
<p>电池允许自检</p>
<ul>
<li>0: 允许</li>
<li>1: 禁止</li>
</ul>
</li>
<li>
<p>电池电压低预告警</p>
<ul>
<li>0: 正常</li>
<li>1: 预告警</li>
</ul>
</li>
<li>
<p>未知</p>
</li>
<li>
<p>开关机状态</p>
<ul>
<li>0: 关机</li>
<li>1: 开机</li>
</ul>
</li>
</ol>
<p>举例请求 <code>44</code> 命令后，设备返回下列字符串（不含起始及结尾符号）：</p>
<p><code>21012A0070360000F00000F000 0E000000000000000000000000000000000000F358</code></p>
<p>数据中每个字节（连续两个字符）表示一个逻辑量。每个逻辑量高 4 位为 <code>0</code> 表示假（正常状态） <code>F</code> 表示真（告警状态）。</p>
<p>各字段含义及错误提示、严重等级如下：</p>
<ol>
<li>未知</li>
<li>未知</li>
<li>主路异常 告警 一般</li>
<li>整流器 故障 严重</li>
<li>逆变器 故障 严重</li>
<li>旁路 告警 一般</li>
<li>电池电压 故障 严重</li>
<li>未知，值为 <code>0E</code></li>
<li>功率模块过温 故障 严重</li>
<li>风扇 告警 一般</li>
<li>输入缺零故障 故障 严重</li>
<li>母线异常关机 故障 严重</li>
<li>充电器 故障 严重</li>
<li>电池放电终止 异常 一般</li>
<li>辅助电源 故障 严重</li>
<li>输出过载 告警 一般</li>
<li>输出线路 短路 严重</li>
<li>过载超时 告警 一般</li>
<li>并机均流异常 告警 一般</li>
<li>并机连接线异常 告警 一般</li>
<li>并机地址错误 告警 一般</li>
<li>内部通讯异常 告警 一般</li>
<li>系统过载 告警 一般</li>
<li>紧急关机 紧急关机 一般</li>
<li>电池接反 告警 一般</li>
<li>电池无 告警 一般</li>
<li>未知</li>
</ol>
<script>
    function cal_checksum(str) {
    	var checksum = (0x10000 - [].slice.call(str).reduce((sum, c) => sum + c.charCodeAt(0), 0)).toString(16).toUpperCase();
        document.getElementById("checksum").value = checksum;
    }
</script>
<div>
    <label>输入（不含起始字符、结束字符、校验码）<br> <input style="width:380px" onchange="cal_checksum(this.value)"></label>
    <br>
    <label>校验码 <br> <input style="width:120px" id="checksum"></label>
</div>
]]></content:encoded></item><item><title><![CDATA[Windows版本WireGuard的配置文件存放在哪里?]]></title><description><![CDATA[<p>先放答案：<br>
<code>C:\Windows\System32\config\systemprofile\AppData\Local\WireGuard\Configurations</code></p>
<p>WireGuard是个很方便的VPN软件，用来连接家里的网络非常顺手，几乎没有障碍，而且官方更新也很频繁，最近也把Windows版做了出来，使用起来很稳定。</p>
<p>最近升级了Windows10到1903版本，但是升级完后发现WireGuard的配置不见了。这个配置文件我也没有保存一份，而且重新生成配置还要改好几个地方的密钥，很麻烦。</p>
<p>Windows在大版本升级后，将原系统文件放到了Windows.old文件夹中，尝试搜索配置文件名，没有找到。然后无意间看到WireGuard界面的Log中有一条<code>&quot;Not migrating configuration from 'C:\windows.old', as it is not explicity owned by SYSTEM, but rather 'S-1-5-32-544'&quot;</code>，基本可以说明配置在那个备份文件夹中还是有的。</p>
<p>但是WireGuard把它的配置放在了哪里？我新建了一个配置，</p>]]></description><link>https://zhujinliang.com/windowsban-ben-wireguardde-pei-zhi-wen-jian-cun-fang-zai-na-li/</link><guid isPermaLink="false">5d2c7558a40233043074cd1a</guid><dc:creator><![CDATA[zhujinliang]]></dc:creator><pubDate>Mon, 15 Jul 2019 13:09:42 GMT</pubDate><content:encoded><![CDATA[<p>先放答案：<br>
<code>C:\Windows\System32\config\systemprofile\AppData\Local\WireGuard\Configurations</code></p>
<p>WireGuard是个很方便的VPN软件，用来连接家里的网络非常顺手，几乎没有障碍，而且官方更新也很频繁，最近也把Windows版做了出来，使用起来很稳定。</p>
<p>最近升级了Windows10到1903版本，但是升级完后发现WireGuard的配置不见了。这个配置文件我也没有保存一份，而且重新生成配置还要改好几个地方的密钥，很麻烦。</p>
<p>Windows在大版本升级后，将原系统文件放到了Windows.old文件夹中，尝试搜索配置文件名，没有找到。然后无意间看到WireGuard界面的Log中有一条<code>&quot;Not migrating configuration from 'C:\windows.old', as it is not explicity owned by SYSTEM, but rather 'S-1-5-32-544'&quot;</code>，基本可以说明配置在那个备份文件夹中还是有的。</p>
<p>但是WireGuard把它的配置放在了哪里？我新建了一个配置，然后尝试Program Files,AppData Local等目录，均没有找到。于是去Github看其源码，在项目中通过关键词<code>&quot;migrating&quot;</code>或<code>&quot;configuration&quot;</code>很容易找到相关代码，最后看到WireGuard确实是将配置文件存放在<code>%APPDATALOCAL%\WireGuard</code>中。猜测权限的问题，尝试从Admin权限的PowerShell进入，提示目录不存在，各种尝试均失败，百思不得其解。</p>
<p>想到WireGuard可能使用高权限用户启动，那是不是用户名不是当前登录的用户名呢，通过Task Manager看到WireGuard有两个进程，一个用户名为当前登录用户名，另一个是<code>SYSTEM</code>，但是<code>Users</code>目录底下并没有<code>SYSTEM</code>目录，更别提再下面的<code>AppData</code>目录了。</p>
<p>Windows不按套路出牌的事不是一件两件了，上网搜索SYSTEM用户的AppData目录在哪里，搜到一篇文章指出System用户的AppData目录在<code>C:\Windows\System32\config\systemprofile\AppData\Local</code>或者<code>C:\Windows\SysWOW64\config\systemprofile\AppData\Local</code>下面，一试果然就是，途中会确认两次权限，进入后看到了<code>WireGuard</code>目录，再下面有<code>Configurations</code>目录，配置文件就在里面，后缀名为<code>.conf.dpapi</code>。终于明白了，赶紧去windows.old目录下，找到之前的配置文件，拷到当前的目录里，再启动WireGuard，配置文件又回来了。</p>
]]></content:encoded></item><item><title><![CDATA[Openwrt 设置串口控制台登录]]></title><description><![CDATA[<p>Openwrt 默认情况下不要求串口控制台（或键盘）登录，通过串口可以直接以 root 身份获得 shell。道理上讲问题不大，毕竟已经物理接触了，再来个密码保护意义不大。当然想打开密码登录的话也是可以的。</p>
<p>网上搜索到的办法是重新编译 busybox，然后修改 /etc/inittab 文件。但我编译了 trunk 版本，登进去看到已经指向了 <code>/usr/libexec/login.sh</code> 文件，该脚本的逻辑为检查 <code>system.@system[0].ttylogin</code> 配置，如果其指为 1，则要求登录，否则直接使用 root 权限。</p>
<p>那么事情就简单多了，执行 <code>uci set system.@system[0].ttylogin=1</code>，退出后再次登录就要求输入用户名和密码了。而且不需要重新编译</p>]]></description><link>https://zhujinliang.com/openwrt-serial-console-password-login/</link><guid isPermaLink="false">5c1cc715c05b425f3be7fcbd</guid><dc:creator><![CDATA[zhujinliang]]></dc:creator><pubDate>Sat, 29 Dec 2018 02:48:36 GMT</pubDate><content:encoded><![CDATA[<p>Openwrt 默认情况下不要求串口控制台（或键盘）登录，通过串口可以直接以 root 身份获得 shell。道理上讲问题不大，毕竟已经物理接触了，再来个密码保护意义不大。当然想打开密码登录的话也是可以的。</p>
<p>网上搜索到的办法是重新编译 busybox，然后修改 /etc/inittab 文件。但我编译了 trunk 版本，登进去看到已经指向了 <code>/usr/libexec/login.sh</code> 文件，该脚本的逻辑为检查 <code>system.@system[0].ttylogin</code> 配置，如果其指为 1，则要求登录，否则直接使用 root 权限。</p>
<p>那么事情就简单多了，执行 <code>uci set system.@system[0].ttylogin=1</code>，退出后再次登录就要求输入用户名和密码了。而且不需要重新编译 busybox。</p>
<p>最后执行 <code>uci commit</code> 保存设置，再次登录时就要求输入用户名和密码了。</p>
]]></content:encoded></item><item><title><![CDATA[使用 OpenSSL 制作自签名 ECC 证书]]></title><description><![CDATA[<h3 id="">一般步骤</h3>
<ul>
<li>
<p>制作私钥<br>
<code>openssl ecparam -out {KeyFile.key} -name prime256v1 -genkey</code></p>
</li>
<li>
<p>制作证书请求<br>
<code>openssl req -key {KeyFile.key} -new -out {ReqFile.req}</code><br>
如果事先做了一个配置，可以在这里使用 <code>-config {ConfigFile.cnf}</code> 参数载入配置</p>
</li>
<li>
<p>制作自签名根证书<br>
<code>openssl x509 -req -in {ReqFile.req} -signkey {KeyFile.key} -out {PEMFile.pem} -days 365</code><br>
可通过 <code>-days</code> 参数设置证书过期时间</p>
</li>
<li>
<p>使用根证书签名<br>
<code>openssl x509 -req -in {ReqFile.</code></p></li></ul>]]></description><link>https://zhujinliang.com/create-ecc-cert-with-openssl/</link><guid isPermaLink="false">5c1a1b3cc05b425f3be7fc3f</guid><dc:creator><![CDATA[zhujinliang]]></dc:creator><pubDate>Wed, 19 Dec 2018 10:27:09 GMT</pubDate><content:encoded><![CDATA[<h3 id="">一般步骤</h3>
<ul>
<li>
<p>制作私钥<br>
<code>openssl ecparam -out {KeyFile.key} -name prime256v1 -genkey</code></p>
</li>
<li>
<p>制作证书请求<br>
<code>openssl req -key {KeyFile.key} -new -out {ReqFile.req}</code><br>
如果事先做了一个配置，可以在这里使用 <code>-config {ConfigFile.cnf}</code> 参数载入配置</p>
</li>
<li>
<p>制作自签名根证书<br>
<code>openssl x509 -req -in {ReqFile.req} -signkey {KeyFile.key} -out {PEMFile.pem} -days 365</code><br>
可通过 <code>-days</code> 参数设置证书过期时间</p>
</li>
<li>
<p>使用根证书签名<br>
<code>openssl x509 -req -in {ReqFile.req} -CA {CA_PEMFile.pem} -CAkey {CA_KeyFile.key} -out {PEMFile.pem} -CAcreateserial -days 365</code></p>
</li>
</ul>
<p>制作证书请求时会询问一些配置，其中最重要的是 <code>CommonName</code>，主要使用这个字段判断证书所有者身份</p>
<h3 id="">实例</h3>
<ol>
<li>
<p>制作自签名根证书，假设根证书我们都命名为 <code>CA.*</code></p>
<p>a. 制作私钥<br>
<code>openssl ecparam -out CA.key -name prime256v1 -genkey</code><br>
b. 制作请求<br>
<code>openssl req -key CA.key -new -out CA.req</code><br>
为了方便记忆，CommonName 我们设置为 <code>ca.system.zhujinliang.com</code><br>
c. 制作自签名证书<br>
<code>openssl x509 -req -in CA.req -signkey CA.key -out CA.pem -days 365</code></p>
<p>CA.pem 就是我们的根证书，CA.key 是私钥，私钥一定要安全保存，一方面签名证书时需要用到私钥，另一方面，私钥丢失就意味着可以被恶意签发证书</p>
</li>
<li>
<p>制作服务器证书，假设服务器证书命名为 <code>sv1.*</code></p>
<p>a. 制作私钥<br>
<code>openssl ecparam -out sv1.key -name prime256v1 -genkey</code><br>
b. 制作请求<br>
<code>openssl req -key sv1.key -new -out sv1.req</code><br>
这里 CommonName 我们设置为 <code>sv1.zhujinliang.com</code><br>
c. 签名证书<br>
<code>openssl x509 -req -in sv1.req -CA CA.pem -CAkey CA.key -out sv1.pem -CAcreateserial -days 365</code></p>
<p>sv1.pem 就是服务器证书，sv1.key 是证书的私钥</p>
<p>签发时会生成 CA.srl 文件，这个是最后一次颁发的证书的序列号，之后生成证书会需要这个文件</p>
</li>
<li>
<p>制作客户端证书，制作过程与制作服务器证书相同，这里假设客户端证书命名为 <code>liang.*</code></p>
<p>a. 制作私钥<br>
<code>openssl ecparam -out liang.key -name prime256v1 -genkey</code><br>
b. 制作请求<br>
<code>openssl req -key liang.key -new -out liang.req</code><br>
这里 CommonName 我们设置为 <code>liang</code><br>
c. 签名证书<br>
<code>openssl x509 -req -in liang.req -CA CA.pem -CAkey CA.key -out liang.pem -CAcreateserial -days 365</code></p>
<p>liang.pem 就是服务器证书，liang.key 是证书的私钥</p>
</li>
</ol>
<h3 id="">查看证书信息</h3>
<p>可以通过命令 <code>openssl x509 -in CA.pem -text</code> 查看证书信息。</p>
]]></content:encoded></item></channel></rss>