一、debug收获
1.软模拟I2C要点
第九时钟应答位,一定要先拉低SCL 再释放SDA 然后拉高SCL,顺序错误可能让一些严格的I2C器件响应失效。
起始和结束也是,应当先拉低SCL,再将SDA放到初态,然后拉高SCL,给定SDA上升和下降沿
2.RGB调色原理
RGB调色是使用PWM和SL调色的,PWM和SL分别代表占空比和电流,实际上单一元素就可以影响,但是这个关系并非RGB的图谱数值,RGB映射到这两个参数上只是一种比例趋势。
一般可以采用pwm最大输出,只调节SL的方式来调色。
3.strcmp和memcmp
strcmp遇到\0会停止
memcmp则不会,memcmp会在检测到第一个不同字符时返回,如果没有就检测到n字符
4.非默认外部晶振与主频
一般MCU的外部晶振为8M/25M,当你使用了非默认频率晶振的时候,有两种解决方案:
<1> 直接修改晶振频率的宏定义,这样做可以保证你能正常配置上主频和波特率等等,
这是由于波特率等获取时钟是从宏定义和寄存器分频倍频系数得到的,所以只要修改了宏定义,
就可以正常配置各个外设。
但是但是,这样调用的函数还是按照默认外部晶振调用的,所以这时候你的实际频率是高于函数名标定的频率的,譬如你使用的是外部8M晶振的72M主频配置函数,但是你实际是12M晶振,那么你最终的主频实际上是72/8*12=108M,,因为函数内部的分频倍频系数是没变的。假设你使用的是最高频函数,此时可能超频导致异常。
<2>修改宏定义的同时,修改函数内部的分频和倍频系数,这样做的好处就是你可以得到你想要的实际频率,譬如目标主频120M,默认配置为8M晶振,那么分频倍频公式如下,8/2*30=120M,
你想要由12M晶振得到120M,,那么需要12/2*20=120M,配置倍频系数为20。
注:由于很多时候大家使用的是STM32的HAL库来写GD32,(STM32主频比GD低,一般GD32F3为120M,STM32F3为72M),为了方便,可以只修改倍频系数,72M的配置函数
在12M晶振时,最终也在GD32F3的主频正常范围内。
5.volatile与编译器优化
关于volatile,在不被外部函数显式改变的情况下,连续调用同一变量,往往会从寄存器直接读取,而非去内存读取,这可能导致你无法读取到最新的值,尤其是只在定时器中改变的一个计数器timecnt。
6.const与形参
const,在函数形参中传入的指针假如你确定不需要修改,可以加上const ,不论是* 还是const *都可以被正确接收,但是返回值请不要用const修饰,这是为了便于后续使用。
7.比较浮点数
浮点数不能直接判断等,一般求差然后<一定范围。
#include <math.h>
/*
abs是绝对值函数(整形)
fabsf是浮点绝对值
*/
int main( void )
{
float a = 1.3f;
float b = 1.2f;
float sub = fabsf( a-b);
if( sub < 0.001f )
{
printf("a is near b");
}
}
8.逗号表达式
在使用逗号的时候,最终结果是以最后一个内容为准的。如下
/*
需要注意的是,逗号表达符只是说该式子结果为最后一部分,
而不是前面的都不执行了。
*/
int main( void )
{
int a = (1,2);
printf("%d",a); //输出为2
}
**************************************
int main( void )
{
int a;
a = 1,2;
printf("%d",a);//输出为1
}
9.指针的特性
/*
字符串本质就是指针,所以可以这么访问。
*/
“0123456789abcdef”[index],这样就相当于直接取字符串索引位置字符,
这是一个将数字直接转化为str的案例
uint8_t int_to_str( uint8_t num )
{
return "0123456789abcdef"[num];
/*
这样和先把字符串赋值给数组,再索引访问是等价的。
*/
}
/*
数组的脚标访问实际上也是指针访问
*/
譬如 int arr[10];
arr[5]等价于 *(&arr[0]+5),同理可以写成5[arr],其实本质上都是指针访问,
基地址+偏移。
10.乘除与移位
当两个数据乘除的时候,乘数或者除数为2的次幂,实际上可以直接变成移位操作,移位比乘除运行更效率,这样写是一种性能优化的写法。
11.SW接口的复用
SW调试接口,一般SW接口是默认使能的,不建议把SW设置成普通IO使用,如果设置了,那么在运行中就没办法使用keil通过SW连接烧录和仿真了,但是这一般可以通过IAP或者ISP烧录新的程序解除,也可以在上电复位期间操作,因为上电复位会重置除了备份寄存器以外的所有部分,SW默认是使能的。
但是但是这不意味着BKP是不会丢失数据的,BKP能保存数据依赖于一个低功耗的电源域,完全断电的情况下,BKP的数据是会丢失的。
12.校验和的取反和异或
在校验和中,按位取反 和 异或0XFFFF全1是一样的结果,大部分时候效率相当,在一些平台上异或甚至做了优化会更快。
异或:
异或就是相反则出1,因此0异或0/1还是0/1,1异或0/1就会取反,一个数异或自己一定是0,利用这种特性,
可以通过一定信息实现查找一堆数字中重复出现的数字。
13.赋值运算顺序
赋值操作是从右往左计算的
14.闭包表达式与宏定义
{}是独立语句块的边界,( )是表达式的边界,可以通过在{ }外加( )的方式使得宏变得具有返回值,
闭包表达式的值是最后一个内容。
譬如
#define RETURN_MACRO(x) ({ x; })
这个宏的返回值就是x,可以这么调用 int a =RETURN_MACRO(5);则a=5;
15.#与##
#是串化标志符号
#define STRING(x) #x
当调用STRING(abc)那么会产生字符串,等价于“abc”,但是直接在宏使用“abc”,
引号里的内容是不可变的
字符串常量可以直接拼接,“ABC”"DEF"和"ABCDEF"是等价的
因此假如你需要一些固定的字符串和不定的字符串使用宏封装,那么可以使用如下形式,譬如
#define STRING(x) "name:"#x
##是宏 连接标识符
#define STRING(x,y) (x##y) 譬如输入 a,b则输出ab,
当宏函数中的参数又有宏时,需要添加中间宏,因为宏一次只能展开一层,
譬如
#define STRING(x,y) (x##y)
#define a this_
#define b is
你想将a,b传入,则需要引入
#define S_STRING(x,y) STRING(x,y)
你可以这样使用
int S_STRING(a,b) = 5;
等价于 int this_is =5;
#串化以后的内容不能与##直接相连,##本身就可以连接标识符,而#后变成了字符串
16.位图
位图排序,使用bit位或者数组代表有或者无,每次读取到数据就丢进位图,然后扫描位图,对应位置>=1就存入新数组,最后就是顺序排列。
17.inline
inline只能在一个文件内使用,不能在.h声明后给其他文件用
18.寄存器与thumb模式
R0是返回值寄存器,LR是返回地址寄存器,PC是当前执行代码地址(以汇编语句地址为单位),SP是栈顶,LR返回弹入PC时,往往差1,地址末位被用作指令集指示位。因为thumb指令的标识就是末尾为1,但是实际地址末尾是0,因为M内核没有ARM模式,所以LR内地址总是以1结尾,PC内地址总是以0结尾。
19.寄存器保存现场
R0-R3 和R12都可以用来保存返回值和传递参数,R1-R11被称为保留寄存器,当使用保留寄存器时,要对其进行入栈操作,进行保存,在返回时出栈还原。
这也和中断时保存现场有关,中断时会依次将XPSR,PC,LR,R12,R3-R0入栈。如果有需要也要将R11-R4入栈,在RTOS中,会在pendsv中断中先入栈XPSR,PC,LR,R12,R3-R0,再入栈R11-R4,这符合AAPCS标准寄存器栈帧的规定。
20.简单汇编
BL跳转入 ,B循环,LDR加载内容到寄存器,STR读取寄存器内容到,[R4]对R4存的地址的内容操作,BX LR将LR寄存器压入PC并跳转,B无条件跳转,一般用于死循环
21.if易错点
在使用if条件语句的时候,假如是一次性语句,记得在进入后,破坏if的条件,避免重复进入。
22.中断的特性
中断服务函数可以直接调用,就像普通函数一样。这是arm cortex的特性。
23.流水线与CPU效率
ARM单片机采用多级流水线机制,可以加快访问效率,但是一旦遇到跳转操作就会丢弃剩余指令,因此减少跳转是优化的关键。.
通过LR寄存器返回时,是返回的原PC+4的地址,是指示的程序下一行。
24.位带操作与消除竞争
位带操作可以消除共享寄存器带来的竞争,实现了每个bit位的的单独操作,避免了资源共享。
25.volatile的特性
volatile 和const一样具有临近修饰的特性,所以假设你目的是修饰一个活动指针,那么你应当如下表示
type * volatile ptr;
二、内存管理与应用
1.内存布局
闪存块布局(扇区个数和大小)可以在用户手册FMC章节获取,对于GD32来说 最小操作单元是页,没有扇区的概念,
一般系统中,内存大小如下:byte->页->扇区->块。外挂的FLASH一般单页较小,以扇区为最小擦除单元,以页为读写单元,
GD32中页较大,没有扇区,所以 页是最小擦除和读写单元。
2.栈开销
栈帧与栈开销:
栈帧记录了函数的必要数据,也是函数被调用时的栈开销,栈开销本身是不包括他调用的其他函数的栈帧的,总的栈开销是同一时刻在运行的函数所有栈帧相加。
少量代码会被编译器优化内联,{}语句块的语句定义的局部变量在函数结束才会被释放而不是在语句块结束释放。
3.内存泄露与溢出
内存泄露:动态内存申请但未释放
内存溢出:栈溢出,数组越界,深度递归(也是栈溢出的一种)
三、bootloader
1.合并烧录boot和app
boot与app合并烧录,首先在keil设置好app和boot的偏移,使用keil生成hex,
使用notepaad++打开文件,将boot末尾的结束字节删除,直接把app的bin加在后面,最后补上一个结束符。
如下、手动合并boot和app 的.hex 文件
(1)设置boot程序下载到flash 的开头地址为0x0800 0000,然后编译程序生成hex文件。
(2)设置APP程序下载到flash 的开头地址(地址依据芯片和程序大小而定),然后编译程序生成hex文件。
(3)用 notepad++ (或Uedit32) 打开 boot 的hex文件和APP的hex 文件
把boot的.hex 最后一句结束语句去掉(即:删除:00000001FF)
把APP的.hex 全部内容拷贝复制到 刚才删掉结束语句的boot的.hex后面
(4)把两个hex合成的hex文件重新命名为XXX.hex,然后通过烧写工具烧写到0x0800 0000 开始位置的地址即可。
2.eeprom和flash
eeprom一般支持字操作(擦写),而flash则不能,mcu内部一般是norflash,只能以页为擦写单位,所以用户数据这类经常变动的数据适合使用eeprom,而一些备份数据由于不变动,则适合写入flash。
bootloader一般是挂载在内部flash的,文件系统一般是挂载在外部内存上的(譬如SD卡和flash)。
四、驱动框架与调试
1.结构体的应用
C99支持结构体变量赋值给另一个结构体变量,也可以在声明的时候直接初始化部分或全部内容,(部分的话可以.访问成员然后赋值),譬如struct type g ={.a=1,.b=1,.d=4};
结构体与占位符(空数组),譬如数据包解析中,数据长度是不定的,可以使用定义data【0】在结构体末尾的方式来占位快捷访问.还可以定义一个不定长数组 arr[ ];使用malloc在创建时动态分配的方式来实现,用完记得释放内存,避免内存泄露。
如果是创建发送数据包也可以。
2.封装与面向对象之驱动注册
首先定义一个结构体,内部包含了属性(就是变量)和行为(就是函数)(由于C没办法定义函数在结构体,所以使用函数指针)。
在开机以后,注册阶段将对应设备的驱动函数赋值给结构体,然后外部只通过结构体的接口来实现对应的行为,这可以统一接口,并且能实现动态加载驱动的功能。
3.策略模式
抽象同类设备的驱动函数接口,然后根据需要注册进结构体,然后通过这个访问的模式,
就是行为设计中的策略模式的一种表现形式。
4.寄存器、形参、堆栈
R0-R3这四个通用寄存器一般用来保存形参,R4-R11用来保存局部变量,R12是在子程序间scratch寄存器,也称为ip寄存器。
R13/SP栈顶指针寄存器,R14/LR链接寄存器,也称返回地址寄存器,R15/PC运行函数寄存器。
5.halt与堆栈分析
halt硬件异常中断与堆栈分析,在进入halt前,cpu会把当前的PC,LR,R12,R3,R2,R1,R0
依次入栈,由于栈的逆向生长结构,实际上SP指向栈的内容,从低地址到高地址依次是R0,R1,R2,R3,R12,LR,PC,这几个寄存器是特殊寄存器,在调用子函数的时候就会更新他们的值,把他们入栈是为了保护现场,便于完成函数或者中断后切回。
当进入halt时,PC寄存器里的内容是进入中断前的函数,LR则是调用该函数的函数,因为PC寄存器在调用一个函数以后,他始终保存调用的子函数调用地址,直到该函数结束返回。
6.自动注册接口
在系统中,函数的驱动和外围器件的CANID是需要动态注册的,器件的CANID可以通过先将所有外围设置为同一个ID,然后扫描该ID依次修改的方式实现。
驱动函数则应当设置一个检测函数,用以识别外挂器件的具体类型,并根据类型将驱动接口注册到到驱动结构体的指针上。
五、小技巧
1.结构体的访问
结构体前128字节访问速度较快
2.switch case
switch case的case case可以是一个范围,如下:
case 1 ... 20:
...和两个数之间要有空格,这样1-20之间的整型都会被识别到这里,但这并不是C的特性,似乎是编译器特性。
3.数组初始化
数组可以从任意地址开始初始化,
int arr[100]={[10]=10,[12]=3};(需要支持C99)
4.typeof
typeof可以直接获取内容的类型,尽管他没法显示出来,但是可以用它定义变量类型
需要gcc 90以上版本,譬如
typeof(a) c;定义一个a同类的c;
5.结构体和位域
在位域中,一些不需要显式显现的部分可以通过不给定变量名的方式隐藏,如下
struct abc
{
uint32_t :4;
uint32_t a :1;
};
代表使用了四个bit占位符。
六、C++
1.C++的类型转化
c++有四种类型转化(c的强制转化也能用)
<1>static_cast 静态转换,主要转换基本类型之间的行为,对于不含有继承关系的结构体和类指针与其他类型的转化也用这个
<2>dynamic_cast 静态转换,主要转换父类与子类指针,转化失效会返回nullptr,会对两者的继承关系做检查,同时要求父类有虚表,假设只继承无虚函数,也不应当使用dynammic——cast,应该使用static_cast
<3>const_cast 用于将const/volatile变量转化为非const/volatile变量,修改其读写属性。
<4>reinterpret_cast 类似C的强制转化,实现最底层(字节级别的)的类型转化。
在C++中 转化使用<>而不是(),同时变量本身也加上( ),例:
int a = static_cast<float>(b);
2.带默认值的形参
C++中,函数的形参可以有默认值,假设有对应的形参传入,则会以传入为准,假设该形参未传入就会使用默认值。
如下
int sum(int a,int b=1)
{
return a+b;
}
如果调用sum(1,5),则返回值6,如果sum(1)则等价于sum(1,1)为2
3.引用与指针
引用的概念类似于指针,在作为函数参数的时候,只消耗较少的栈,但是引用比指针更安全,他会更严格的检查类型匹配。
引用是固定绑定的,一旦绑定就无法再修改,而指针是可以变化的,所以引用比指针更安全。
4.类与成员函数
在类中,成员函数优先查找成员变量使用,在没有同名成员变量才调用外部全局变量,如果都没定义就报错,一般推荐显式的使用全局变量 如 ::a = 5;对全局变量a赋值5。成员函数可以在类内直接定义,也可以在类外定义。
如下
int k = 0;
class aaa
{
pubilc:
void set(int a);
private:
int k;
}
void aaa::set(int a)
{
/* 将a赋值给全局k,而后将a赋值给类内私有k*/
::k = a;
k = a;
}
5.类与继承
有public, protected, private三种继承方式,它们相应地改变了基类成员的访问属性。
1.public 继承:基类 public 成员,protected 成员,private 成员的访问属性
在派生类中分别变成:public, protected, private
2.protected 继承:基类 public 成员,protected 成员,private 成员的访问
属性在派生类中分别变成:protected, protected, private
3.private 继承:基类 public 成员,protected 成员,private 成员的访问属
性在派生类中分别变成:private, private, private
但无论哪种继承方式,下面两点都没有改变:
1.private 成员只能被本类成员(类内)和友元访问,不能被派生类访问;
2.protected 成员可以被派生类访问。
总结就是 优先级 private > protected > public ,当使用属性继承时,会将低于此优先级
的成员转化为子类的当前继承优先级成员。
即 私有继承全私有,保护继承无公有,公有继承属性都不变。
6.class与struct
在C++中,不声明类的成员访问属性,则默认为私有private,结构体中,则默认为公有public,继承自二者的子类同理。
七、专业英语
1.英语小笔记
sector 扇区
seek查找
path路径
freq频率
blcok块
N、附录 更新日志
2024/7/17
7月下半开篇,更新了一些debug内容。
2024/7/18
更新了一些debug内容和一些语法内容
2024/7/20
将19日和20日内容一并更新了,新开了一些内容。
2024/7/24
将22日至24日内容合并更新了,增加了一些小技巧和C++内容