yzhkpli 发表于 2025-4-17 20:03:42

看了吴坚鸿老师的从单片机基础到程序框架的笔记

前两天刚在douyin上看到吴老师的介绍视频。正在从头从C语言基础开始看。
之前看了b站很多高手的视频。各种方法,感觉意犹未尽。打算看看一份系统的。

吴老师的教程是多年前开始写,修改了好几次。确实比较有质量,但是不代表没有纰漏{:4_202:}

我以我孱弱的c语言基础,一边实践,一边记录一下。
测试环境是win10 64位+dev-c++,所以用的是printf直接输出变量的值,而不是用了教程的View()通过串口输出。这样更利于快速学习c的语法

他的教程可以在 dumenmen.com上下载。无需注册即可下载。
网站名字来自于道德经。

知乎专栏
https://zhuanlan.zhihu.com

《道德经》二十章“众人昭昭,我独昏昏。众人察察, …
2024年1月5日 · “众人昭昭,我独昏昏。众人察察,我独闷闷。”众人表现出精明锐利,而我独像一个糊涂人,无知无识的样子。众人皆一副苛察的样子,而我独自闷闷然无所分别的样子。不去计较,也不追逐,不去陶醉,也不抗衡,只是静

yzhkpli 发表于 2025-4-17 20:06:36

P266
第七十五节: 指针的名义(例: 一维指针操作二维数组)。

教程原文:
我曾遇到这样一种情况, 要从一个二维表格里提取某一行数据用来显示,
而这个显示函数是别人封装好的一个库函数, 库函数对外的接口是一维指针,
这样, 如何把二维表格(二维数组) 跟一维指针在接口上兼容起来, 就是一个要面临的问题,
这时有两种思路,
一种是把二维数组的某一行数据先用原始的办法提取出来存放在一个中间变量的一维数组,
然后再把这个一维数组代入到一维指针接口的库函数里,
另一种思路是绕开中间变量, 直接把二维数组的某一行的地址强制转换成一维指针的类型, 利用
“类型强制转换” 绕开 C 编译器的报错或警告, 实现二维数组跟一维指针“直通”,
经过实验, 这种方法果然可以,

我使用的是
#include "stdio.h"

unsigned char table[]={
{0x00,0x01,0x02},
{0x10,0x11,0x12},
{0x20,0x21,0x22}
};
unsigned char *pGu8;

unsigned char Gu8Buffer;
unsigned char i;

int main(void){
        pGu8=&table;
        for(i=0;i<3;i++){
                Gu8Buffer=pGu8;
                printf("%d\t",Gu8Buffer);
        }
        return 0;
}

跟教程上的pGu8=(unsigned char *)&table;不同。

咨询了一下通义千问

实际上,在你的代码示例中,pGu8 = &table; 和 pGu8 = (unsigned char *)&table; 都是将 pGu8 指向二维数组 table 的第三个子数组(即索引为2的那个子数组)的第一个元素。然而,这两种方式在实际操作上几乎没有任何区别,因为 &table 已经是一个 unsigned char* 类型的指针了,因此不需要强制类型转换。

解释
pGu8 = &table; 这行代码直接取二维数组 table 中第三个子数组的第一个元素的地址,并将其赋值给 pGu8。由于 table 是一个 unsigned char 类型的变量,&table 就是一个指向 unsigned char 的指针,这正好与 pGu8 的类型匹配,因此不需要进行类型转换。
pGu8 = (unsigned char*)&table; 在这种情况下,虽然你进行了显式的类型转换 (unsigned char*),但实际上并没有改变任何东西,因为 &table 本来就是一个 unsigned char* 类型的指针。这种类型的转换在这里是多余的,但它不会导致错误或改变程序的行为。

这是因为 table 是 {0x20, 0x21, 0x22},其对应的十进制值分别为 32, 33, 34。无论你是使用 pGu8 = &table; 还是 pGu8 = (unsigned char*)&table;,结果都是相同的,因为在这两种情况下,pGu8 都是指向 table 的指针,允许你通过它访问 table 子数组的所有元素。

总结
如果目标指针类型和源表达式的类型相同,则不需要进行显式类型转换。
在上述情况下,pGu8 = &table; 足够了,而添加 (unsigned char*) 强制类型转换并不会改变程序行为,但也没有必要。
了解何时需要类型转换以及如何正确应用它们对于编写清晰且无误的C语言代码非常重要。但在本例中,这种转换是多余的。

yzhkpli 发表于 2025-4-17 20:56:22

P269
第七十六节: 二维数组的指针。
【76.1 二维数组指针的用途。】
前面章节讲了一维指针操作二维数组, 本质是通过“类型强制转换” 实现的, 这种应用局限于某些特定的
场合, 毕竟一维有 1 个下标, 二维有 2 个下标, 一维和二维在队形感上是有明显差别的, 强行用一维指针操作
二维数组会破坏了代码原有的队形感, 大多数的情况, 还是用二维指针操作二维数组。
二维指针主要应用在两个方面, 一方面是 N 个二维数组的“中转站” 应用, 另一方面是函数接口的应用。
比如, 当某项目有 N 个二维数组表格时, 要通过某个变量来切换处理某个特定的表格, 以便实现“N 选一” 的功能,
此时, 二维指针在这 N 个二维数组之间就起到中转站的作用。 又, 当某个函数接口想输入或者输出一个二维数组时, 就必然要用到二维指针作为函数的接口参数。

76.2
举一个例子, 有 3 个现有的二维数组, 通过某个变量来选择切换, 把某个二维数组的数据复制到指定的一个缓存数组中

#include "stdio.h"

const unsigned char table_1={{0x00,0x01,0x02},{0x10,0x11,0x12},{0x20,0x21,0x22}};

const unsigned char table_2={{0xA0,0xA1,0xA2},{0xB0,0xB1,0xB2},{0xC0,0xC1,0xC2}};

const unsigned char table_3={{0xD0,0xD1,0xD2},{0xE0,0xE1,0xE2},{0xF0,0xF1,0xF2}};

//一个缓存数组
unsigned char SaveBuffer;
//选择变量
unsigned char TableSec;

//中转站的二维指针?这是一个中转用的指针数组吧
const unsigned char (*pTable);
//unsigned char (*pTable);

int main(){
      //选择第二个现有数组
      TableSec=2;
      switch(TableSec){
                case 1:pTable=table_1;break;
                case 2:pTable=table_2;break;               
                case 3:pTable=table_3;break;               
      }
      //遍历指针数组pTable,将数据传递给 SaveBuffer[][]
      unsigned char R,L;
      for(R=0;R<3;R++){
                for(L=0;L<3;L++){
                        SaveBuffer=pTable+2;
                }
      }
      for(R=0;R<3;R++){
                for(L=0;L<3;L++){
                        //SaveBuffer=pTable;
                        printf("%02X ",SaveBuffer);
                }
                printf("\n");
      }
      return 0;
}
执行结果
A2 A3 A4
B2 B3 B4
C2 C3 C4

注意,const unsigned char (*pTable);既不是二维指针,也不是指针数组

二维指针 (unsigned char **p):
      这是一个指向指针的指针。它可以用于指向一个指针数组,而这些指针又各自指向不同类型的数据(例如,一维数组)。

指针数组 (unsigned char *arrayOfPointers[]) 或者更具体地 unsigned char *arrayOfPointers:      
      这是一个数组,其元素是指向 unsigned char 的指针。这意味着你可以有多个指针,每个都指向不同的 unsigned char 数据。
      
这个其实是
指向数组的指针 (unsigned char (*pArray)):
      如你所用,这是一个指向包含 3 个 unsigned char 元素的数组的指针。这种类型的指针可以用来指向整个二维数组的一行,或者任何其他具有相同结构的数组。

      const unsigned char (*pTable);:
      这里的 (*pTable) 表示 pTable 是一个指针。

       表示该指针指向的对象是一个包含 3 个元素的数组。
      const unsigned char 表示这个数组中的每个元素是 const unsigned char类型,即这些元素是不可修改的无符号字符类型。
      因此,pTable 是一个指向具有 3 个 unsigned char 元素的数组的指针。
      它并不是一个“二维指针”也不是“指针数组”。

又问:pTable是一个指向三个元素的数组的指针,为什么可以用pTable这种二维数组的形式?
tongyiqianwen的回答:
      你提到的 pTable 是一个指向包含 3 个 unsigned char 元素的数组的指针。尽管从声明上看,它似乎只是一个指向特定大小数组的指针,但在C语言中,你可以通过多维数组的索引方式来访问数据,这是因为C语言中的数组和指针操作有一些内在的关联。
#include <stdio.h>

int main() {
    // 定义一个二维数组
    const unsigned char table_1 = {{0x00,0x01,0x02},{0x10,0x11,0x12},{0x20,0x21,0x22}};
   
    // 指向含有3个 unsigned char 元素的数组的指针
    const unsigned char (*pTable);
   
    // 将 pTable 指向 table_1 的第1行
    pTable = table_1;
    // 将 pTable 指向 table_1 的第2行
    //pTable = table_1;
   
    // 打印 pTable 指向的内容
    unsigned char i;
    for (i = 0; i < 3; ++i) {
      printf("%02X ", (*pTable));//00 01 02
    }
    printf("\n");
      return 0;
}
对于

const unsigned char (*pTable);
tongyiqianwen的解答:
这意味着 pTable 是一个指向含有 3 个 unsigned char 元素的数组的指针。
当你使用 pTable 访问元素时,实际上是利用了C语言中对指针进行偏移计算的能力。
如何工作的
理解 pTable:
在C语言中,pTable 实际上等价于 *(pTable + R)。这意味着指针 pTable 向前移动 R 次,每次移动的距离是一个包含 3 个 unsigned char 的数组的大小(即 3 * sizeof(unsigned char) 字节)。
因此,pTable 将指向第 R 行的第一个元素。

理解 pTable:
接下来,pTable 实际上是 *(pTable + L) 或者 *(*(pTable + R) + L)。这意味着在已经定位到第 R 行的基础上,再向前移动 L 个 unsigned char 单位。
结果就是,你能够访问位于 R 行 L 列的元素

yzhkpli 发表于 2025-4-22 16:43:59


P46出了问题。
使用提供的文件建立的project
串口

结果右上角的区域仅仅显示

接收←
                  十六进制:3B9ACA00
                  
接收←二进制:111011100110101100101000000000

也就是只有c变量被显示出来了

yzhkpli 发表于 2025-4-22 16:52:10

又出问题了

/*---C 语言学习区域的开始。 -----------------------------------------------*/
void main() //主函数
{
        unsigned char a; //定义一个变量 a。
        a=0x8765; //给变量 a 赋值。

        View(a); //把第 1 个数 a 发送到电脑端的串口助手软件上观察。

        while(1)
        { }
}
/*---C 语言学习区域的结束。 -----------------------------------------------*/
改成只有一个变量,烧写后根本不显示了。。。

yzhkpli 发表于 2025-4-22 17:04:35

P76
第二十节: 隐藏中间变量为何物?

unsigned long d;
unsigned long e;
unsigned int x3=50000;
d=x3+30000;
e=x3+50000;
运算结果: d 等于 14464,e 等于 100000



似乎可以(第37.3就是这么做的)
d=(unsiged long)x3+3000;

使用dev-c++
#include "stdio.h"

int main(void){
      unsigned long d;
      unsigned long e;
      unsigned int x3=50000;
      d=(unsigned long)x3+30000;
      e=x3+50000;
      printf("%d,%d",d,e);//80000,100000
}

yzhkpli 发表于 2025-4-22 18:09:18

P96
26.4 乘法的溢出。】
乘法的溢出规律跟加减法的溢出规律是一样的。 举一个例子如下:
unsigned char m=30;
unsigned char n=10;
unsigned char a;
a=m*n;

应该防止溢出,应该设置类型转换

#include "stdio.h"

int main(void){
        unsigned char m=30;
        unsigned char n=10;
        unsigned char a;
        a=(unsigned long)m*n;
        printf("%d",a);//44
}

设置类型转换后
#include "stdio.h"

int main(void){
        unsigned char m=30;
        unsigned char n=10;
        unsigned long a;
        a=(unsigned long)m*n;
        printf("%d",a);//300
}

yzhkpli 发表于 2025-4-22 18:09:57

以下全是涉及位运算


P106
第二十九节: “先余后商” 和“先商后余” 提取数据某位, 哪家强?

29.1 先余后商
第一步: 先把 8562 拆分成 8562,562,62,2 这四个数。 怎么拆分呢? 用求余的算法。 比如:
        8562 等于 8562%10000;
        562 等于 8562%1000;
        62 等于 8562%100;
        2 等于 8562%10;
第二步: 再从 8562,562,62,2 这四个数中分别提取 8,5,6,2 这四个数。 怎么提取呢? 用求商的算法。 比如:
        8 等于 8562/1000;
        5 等于 562/100;
        6 等于 62/10;
        2 等于 2/1;
第三步: 最后, 把第一步和第二步的处理思路连写在一起如下:
        8 等于 8562%10000/1000;
        5 等于 8562%1000/100;
        6 等于 8562%100/10;
        2 等于 8562%10/1;

29.2 先商后余。
第一步: 先把 8562 拆分成 8,85,856,8562 这四个数。 怎么拆分呢? 用求商的算法。 比如:
        8 等于 8562/1000;
        85 等于 8562/100;
        856 等于 8562/10;
        8562 等于 8562/1;
第二步: 再从 8,85,856,8562 这四个数中分别提取 8,5,6,2 这四个数。 怎么提取呢? 用求余的算法。
        8 等于 8%10;
        5 等于 85%10;
        6 等于 856%10;
        2 等于 8562%10;
第三步: 最后, 把第一步和第二步的处理思路连写在一起如下:
        8 等于 8562/1000%10;
        5 等于 8562/100%10;
        6 等于 8562/10%10;
        2 等于 8562/1%10;

如果先求余再求商
3100000000%10000000000/1000000000
其中10000000000超过了unsigned long的最大数字4294967295

但是先求商再求余,就不会碰到这种问题。
3 等于 3100000000/1000000000%10;

再看看具体的
unsigned int x=8562; //被提取的数
//第一种: 先余后商。
a=x%10000/1000; //提取千位
b=x%1000/100; //提取百位
c=x%100/10; //提取十位
d=x%10/1; //提取个位

//第二种: 先商后余。
e=x/1000%10; //提取千位
f=x/100%10; //提取百位
g=x/10%10; //提取十位
h=x/1%10; //提取个位


P113

用位与清零
b=b&0XFE;//对b的最后一位清零

用位或来置一
b=b|0X01;//对b的最后一位置一


P115
第32节异或
        0^0=1        1^1=0
        0^1=0        1^0=0

自异或
        unsigned char c=9;
        c^=5;



P119
第三十四节: 移位运算的左移

一条左移语句的运算速度比一条乘法语句的运算速度要快很多倍。

        比如有两个 unsigned char 单字节的类型数据 H 和 L,
        H 的初始值是十六进制的 0x12,
        L 的初始值是十六进制的 0x34,
        要将两个单字节的 H 和 L 合并成一个 unsigned int 双字节的数据 c,
        其中 H 是高 8 位字节, L 是低8 位字节, 合并成 c 后, c 的值应该是十六进制的 0x1234,
        此程序如何写? 就需要用到左移。 程序分析如下

        unsigned char H=0x12; //单字节
        unsigned char L=0x34; //单字节
        unsigned int c; //双字节
        c=H; //c 的低 8 位被 H 覆盖, 也就是 c 的低 8 位得到了 H 的值。
        c=c<<8; //及时把 c 的低 8 位移动到高 8 位, 同时 c 原来的低 8 位被填入 0
        c=c+L; //此时 c 再加 L, c 的低 8 位就 L 的值。

34.4 “左移” 的常见应用之二: 聚焦在某个变量的某个位

“或” 运算最常见的用途是可以指定一个变量的某位置 1, 其它位保持不变。
比如一个 unsigned char 类型的变量 b, 数据长度一共是 8 位, 从右往左:

        想让第 0 位置 1, 其它位保持不变, 只需跟十六进制的 0x01 相“或” :b=b|0x01。
        想让第 1 位置 1, 其它位保持不变, 只需跟十六进制的 0x02 相“或” :b=b|0x02。
        想让第 2 位置 1, 其它位保持不变, 只需跟十六进制的 0x04 相“或” :b=b|0x04。
        想让第 3 位置 1, 其它位保持不变, 只需跟十六进制的 0x08 相“或” :b=b|0x08。
        想让第 4 位置 1, 其它位保持不变, 只需跟十六进制的 0x10 相“或” :b=b|0x10。
        想让第 5 位置 1, 其它位保持不变, 只需跟十六进制的 0x20 相“或” :b=b|0x20。
        想让第 6 位置 1, 其它位保持不变, 只需跟十六进制的 0x40 相“或” :b=b|0x40。
        想让第 7 位置 1, 其它位保持不变, 只需跟十六进制的 0x80 相“或” :b=b|0x80

如果把这些十六进制的数值换成左移的写法,
在阅读上就非常清晰直观了。 比如: 0x01 可以用 1<<0 替代, 0x02 可以用 1<<1 替代, 0x04 可以用 1<<2 替
代......0x80 可以用 1<<7 替代。 左移的 n 位, n 就恰好代表了某个变量的某个位。
于是, 我们把上面的片段更改成左移的写法后, 如下:

“或” 运算最常见的用途是可以指定一个变量的某位置 1, 其它位保持不变。 比如一个 unsigned char 类
型的变量 b, 数据长度一共是 8 位, 从右往左:
        想让第 0 位置 1, 其它位保持不变, 只需:b=b|(1<<0)。
        想让第 1 位置 1, 其它位保持不变, 只需:b=b|(1<<1)。
        想让第 2 位置 1, 其它位保持不变, 只需:b=b|(1<<2)。
        想让第 3 位置 1, 其它位保持不变, 只需:b=b|(1<<3)。
        想让第 4 位置 1, 其它位保持不变, 只需:b=b|(1<<4)。
        想让第 5 位置 1, 其它位保持不变, 只需:b=b|(1<<5)。
        想让第 6 位置 1, 其它位保持不变, 只需:b=b|(1<<6)。
        想让第 7 位置 1, 其它位保持不变, 只需:b=b|(1<<7)。

假设 d 原来等于十进制的 84(十六进制是 0x54, 二进制是 01010100), 要想把此数据的第0位置 1, 只需 d=d|(1<<0)。
最终 d 的运算结果是十进制是 85(十六进制是 0x55, 二进制是 01010101)

“与”运算最常见的用途是可以指定一个变量二进制格式的某位清零, 其它位保持不变。 比如一个 unsigned
char 类型的变量 b, 数据长度一共是 8 位, 从右往左:
        想让第 0 位清零, 其它位保持不变, 只需跟十六进制的 0xfe 相“与” :b=b&0xfe。
        想让第 1 位清零, 其它位保持不变, 只需跟十六进制的 0xfd 相“与” :b=b&0xfd。
        想让第 2 位清零, 其它位保持不变, 只需跟十六进制的 0xfb 相“与” :b=b&0xfb。
        想让第 3 位清零, 其它位保持不变, 只需跟十六进制的 0xf7 相“与” :b=b&0xf7。
        想让第 4 位清零, 其它位保持不变, 只需跟十六进制的 0xef 相“与” :b=b&0xef。
        想让第 5 位清零, 其它位保持不变, 只需跟十六进制的 0xdf 相“与” :b=b&0xdf。
        想让第 6 位清零, 其它位保持不变, 只需跟十六进制的 0xbf 相“与” :b=b&0xbf。
        想让第 7 位清零, 其它位保持不变, 只需跟十六进制的 0x7f 相“与” :b=b&0x7f

或者
一个 unsigned
char 类型的变量 b, 数据长度一共是 8 位, 从右往左:
        想让第 0 位清零, 其它位保持不变, 只需:b=b&(~(1<<0))。
        想让第 1 位清零, 其它位保持不变, 只需:b=b&(~(1<<1))。
        想让第 2 位清零, 其它位保持不变, 只需:b=b&(~(1<<2))。
        想让第 3 位清零, 其它位保持不变, 只需:b=b&(~(1<<3))。
        想让第 4 位清零, 其它位保持不变, 只需:b=b&(~(1<<4))。
        想让第 5 位清零, 其它位保持不变, 只需:b=b&(~(1<<5))。
        想让第 6 位清零, 其它位保持不变, 只需:b=b&(~(1<<6))。
        想让第 7 位清零, 其它位保持不变, 只需:b=b&(~(1<<7))

假设 e 原来等于十进制的 85(十六进制是 0x55, 二进制是 01010101), 要想把此数据的第0 位清零, 只需 e=e&(~(1<<0))。
最终 e 的运算结果是十进制是 84(十六进制是 0x54, 二进制是 01010100)



很多 32 位的单片机在初始化寄存器的库函数里大量应用这种左移的方法来操作, 目的就是为了增加代码可读性


P122总结
//左移运算中蕴含着乘 2 的规律。
a=a<<1; //a 左移 1 位, 相当于 a=a*2, 从原来的 5 变成了 10。
b=b<<2; //b 左移 2 位, 相当于 b=b*2*2, 从原来的 5 变成了 20。
//左移的应用之一: 不同变量类型的合并。
c=H; //c 的低 8 位被 H 覆盖, 也就是此时 c 的低 8 位得到了 H 的各位值。
c=c<<8; //及时把 c 的低 8 位移动到高 8 位, 同时 c 原来的低 8 位被填入 0
c=c+L; //此时 c 再加 L, c 的低 8 位就 L 的值。 此时 c 得到了 H 和 L 合并而来的值。
//左移的应用之二: 聚焦在某个变量的某个位。
d=d|(1<<0); //对第 0 位置 1。
e=e&(~(1<<0)); //对第 0 位清零。
//左移简写。
f<<=1; //就相当于 f=f<<1;
g<<=2; //就相当于 g=g<<2;


第三十五节: 移位运算的右移。P124

unsigned char H; //单字节
unsigned char L; //单字节
unsigned int c=0x1234; //双字节
L=c; //c 的低 8 位直接赋值给单字节的 L---注意,这里并没有溢出报错!!
H=c>>8; //c 先把高 8 位右移到低 8 位, 然后再把这 8 位数据赋值给 H


#include "stdio.h"

int main(void){

        unsigned char H; //单字节
        unsigned char L; //单字节
        unsigned int c=0x1234; //双字节
        L=c; //c 的低 8 位直接赋值给单字节的 L
        H=c>>8; //c 先把高 8 位右移到低 8 位, 然后再把这 8 位数据赋值给 H
       
        printf("%#X,%#X",H,L);//0X12,0X34
}


在C语言中,当你将一个较大类型的数据赋值给较小类型的变量时,会发生截断。具体来说,在你的代码里,`unsigned int c=0x1234;` 定义了一个16位的无符号整型变量 `c`,其值为 `0x1234`。然后你执行了两个操作:

1. `L=c;`
2. `H=c>>8;`

对于第一个操作 `L=c;`,因为 `L` 是一个8位的 `unsigned char` 类型(通常为8位),而 `c` 是一个16位的 `unsigned int`。在这种情况下,只有 `c` 的低8位(即 `0x34`)被赋值给 `L`,高8位则被自然地忽略了。这不是溢出,而是数据截断——只使用了适合目标变量大小的部分数据。所以,`L` 得到了 `c` 的低8位的值 `0x34`。

对于第二个操作 `H=c>>8;`,这里通过右移8位操作将 `c` 的高8位移动到低8位的位置,然后再赋值给 `H`。这样,`H` 就得到了 `c` 高8位的值 `0x12`。

综上所述,`L=c;` 并没有造成溢出是因为这是预期的行为:
        从较大的数据类型向较小的数据类型赋值时,仅使用源操作数的一部分(本例中是低8位),而不会产生错误或者异常。这种机制允许程序有效地处理和转换不同大小的数据类型。



P127
第三十六节: 括号的强制功能---改变运算优先级

a=a<<2+5;
运算符 << 的优先级低于加法运算符 +。因此,表达式会被解释为 a = a << (2 + 5),即 a = a << 7。

让我们逐步分析这个过程:

初始值 a = 5,其二进制表示为 00000101。
表达式 2 + 5 计算结果为 7。
然后执行 a = a << 7,即将 00000101 左移 7 位,得到 10000000。
值 10000000 是有符号数 -128 或无符号数 128 的二进制表示,但由于 a 被声明为 unsigned char,所以输出的是 128。



页: [1]
查看完整版本: 看了吴坚鸿老师的从单片机基础到程序框架的笔记