深度掌握C语言的重点模块1.位操作与寄存器编程位运算符|按位或OR有1则为1。常用于置1按位与AND全1才为1有0则为0。常用于清0或掩码~按位 取反NOT)0变11变0.^按位异或XOR相同为0不同为1。常用于翻转左移:将二进制整体向左移动右边补0。比如13就是把0001变成1000也就是十进制的8。配置寄存器的操作操作特定位铁律改变你想要改变的位绝不能影响其他位假设我们有一个8位的寄存器REG0b10100101,我们要操作它的第n位从右往左数最右边是第0位1.将特定位置1Set Bit公式REG | (1U n)原理1UU是Unsigned的意思 n会生成一个只有第n位是1其它全是0的数。将它与REG进行“或”运算根据“有1则1”的原则REG的第n位必然变成1而其它位因为和0进行或运算保持原样2.将特定位清0Clear Bit)公式REG ~1U n原理1.1U n:先生成只有第n位为1的数2.~(1U n):取反得到一个只有第n位是0其它全是1的数掩码3.把这个数跟REG进行“与”运算。根据“有0则0”的原则第n位必然变成0而其他位因为和1进行与运算保持原样。3.将特定位翻转(Toggle Bit)公式REG ^ (1U n)原理利用异或的特性。某一位与1异或原来的1会变成0原来的0会变成1指针强转与寄存器映射代码#defineGPIoA_ODR(*(volatileuint32_t*)0X40020014)假设你查阅芯片的数据手册手册上说“GPIOA的输出数据寄存器地址在0X40020014它是一个32位的寄存器。”编译器只认识代码不知道什么是地址我们要用C语言去读写这个具体的物理地址。1.0x40020014仅仅是一个十六进制的整数2.uint32_t*)0x40020014使用强制类型转换告诉编译器“请把这个数字当成一个内存指针它指向一个32位的无符号整数数据”3.volatile uint32_t*)0x40020014加上volatile告诉编译器“这个地址里的值受硬件控制随时会变千万别做缓存优化每次都要老老实实去这个地址读写”4.**(volatile unit32_t*)0x40020014在最前面加个*解引用运算符意思是“获取这个指针指向的具体内容/空间”。5.用#define GPIOA_ODR给它起个好记的名字这就完成了映射以后你在代码里写GPIOA_ODR 0xFF;C语言就会精准地向内存地址0x40020014写入0xFF,从而控制真实的硬件引脚补充说明给寄存器的某一位写1–拉高Set/High–引脚输出3.3V电压相当于提高电源正极给寄存器的某一位写0–拉低Clear/Low–引脚输出0V电压先当与链接电源负极/地练习代码#defineGPIOA_ODR(*(valatileuniit32_t*)0x40020014)#defineSET_PIN(pin)(GPIOA_ODR|(1Upin))#defineCLEAR_PIN(pin)(GPIOA_ODR~(1Upin))点亮GPIOA的第5号引脚的LED:SET_PIN(5);编译展开成(*(volatileunit32_t*)0x40020014)|(1U5);这样硬件电路检测到寄存器第5位变成1输出高电平LED亮起其他引脚状态完全不受影响熄灭GPIOA的第5号引脚的LED:CLEAR_PIN(5);编译展开成(*(volatileunit32_t*)0x40020014)~(1Upin)这样硬件电路检测到寄存器第5位变成0输出低电平LED熄灭其他引脚状态完全不受影响2.结构体位域与寄存器映射为什么学习寄存器位域假设我们要配置一个GPIO引脚它不仅仅有“高/低”电平它的配置寄存器可能长这样一个32位的今存器被拆成了多个小段第0-1位控制模式00输入01输出10复用…第2位控制输出类型0推挽1开漏第3-4位控制输出速度00低速01中速…如果用上面的方法想把模式设为01速度设为01代码要这样写REG(REG~0x1D)|(0x010)|(0x013);(REG ~0x1D)意思是先把REG要修改的区域进行擦除变成0(0x01 0)和 (0x01 3) 意思是逐步用擦除后的板子进行修改成我们想要的样子但是这个方法有个缺陷过于冗长不容易看懂而且非常容易算错位移的数量所以我们要让代码“说人话”C语言提供了一个高级武器——位域Bit-field位域typedefstruct{unit32_tMODER:2;unit32_tOTYPER:1;unit32_tOSPEEDR:2;}GPIO;在普通的C语言结构体中声明一个unit32_t就会直接占用32个bit4个字节。但是在变量名后面加上冒号和数字例如2它告诉编译器:虽然我声明的是32位的数据类型但MODER这个变量只需要占用2个bit的空间OTYPER占用1个bitOSPEEDR占用2个bit“。编译器非常聪明他就会把这些变量像拼积木一样紧紧地打包在一起内存的第0-1位被分配给了MODER内存的第2位被分配给了OTYPER内存的第3-4位被分配给了OSPEEDR这就是位域寄存器映射结构体定义好了怎么让它和真实的物理地址挂钩呢假设GPIO的端口地址是0x40020000,//1.将物理地址强转为指向该结构体的指针GPIO*gpio(GPIO*)0x40020000;//2.直接向操作面向对象里的属性一样去配置硬件gpio-MODER1;//设置为输出模式底层自动变成01gpio-OTYPER0;//设置为推挽输出底层自动变成0gpio-OSPEEDR1;//设置为中速底层自动变成01当编译器看到gpio-OSPEEDR1;gpio-OTYPER0;gpio-OSPEEDR1时那些繁琐的|~运算并没有消失它们只是被编译器接管了。但是你不用自己手动去写了它会按照你的要求帮你生成你想要修改成的样子注意当你要写入的数所占用的位数大于所分配的位数那么就只会写入低位总结优点极大提高了代码的可读性和可维护性。需要注意的坑C语言标准并没有规定位域在内存中是从左往右排还是从右往左排这取决于编译器和大小端模式。因此跨平台移植含有位域的代码时需要特别小心。不过在特定的单片机开发环境如ARM GCC STM32下排列顺序是确定的。3.函数指针与回调机制函数指针函数指针里面存的是一段代码函数在内存中的起始地址。有了函数指针函数就不再是只能被“写死”调用的 死板东西了它可以像普通变量一样被赋值、被当作参数传递、被存进数组里。typedefvoid(*ISR_Callback)(void);ISR_Callback timer_callbacks[10];typedef:意思是我们要给某种复杂的数据类型起一个“别名”左边的void:规定看这个指针只能指向没有返回值的函数*ISR_Callback):这里的ISR_Callback这就是我们起的新名字。 *表示它是一个指针。右边的void规定了这个指针只能指向不需要传递参数的函数ISR_Callback timer_callbacks[10];这个是一个ISR_Callback类型的数组回调机制Callback)有这样一个场景单片机里有一个硬件定时器Timer。时间一到硬件就会强行打断CPU去执行一个底层写死的函数中断服务函数普通的写法强耦合直接在底层的中断函数里写业务逻辑voidHardware_Timer_INtertupt(void){LED_Toggle();//翻转LED灯}缺点如果有一天需求变了”定时器时间到了不闪灯了改为控制电机转动。“你必须要去修改底层的驱动代码底层驱动和上层应用深深地绑死在了一起强耦合代码根本无法复用高手的写法回调机制解耦底层驱动维护数组。它不知道时间到了该干嘛它只负责“查字典”。voidHardware_Timer_Interrupt(void){//检查第0个坑位有没有登记函数如果有就执行它if(timer_callbacks[0]NULL){timer_callbacks[0]();}}上层的应用代码可以随便改voidMy_Motor_Control(void){//控制电机转动的代码...}intmain(){//注册回调函数timer_callbacks[0]My_Motor_Control;while(1){}}我只要把函数指针数组对应位置的地址改成我要执行的函数地址就可以做到“解耦”4.volatile与内存屏障Volatileunit32_t*status_reg;_asm_volatile(dmb);//数据内存屏障volatile和内存屏障这两个概念存在的原因是编译器和CPU太聪明了聪明到有时会“自作主张”假设你写了这样一段等待硬件准备就绪的代码unit32_t*status_reg(unit32_t*)0x40001000;//假设这是硬件寄存器while(*status_reg0){//苦苦等待硬件把这个寄存器变成1}printf(硬件准备好了);这时我们聪明的C编译器看到死循环里没有任何代码修改*status_reg的值他就觉得每次都去物理内存里面读取太慢太傻了就自作主张把第一次读到的值缓存到CPU内部超快的寄存器里面以后一直判断缓存的值。这样我们后面硬件在1ms后悄悄把物理内存里的位置的值改变了但是CPU还在比对的是寄存器里面的值旧数据就会导致程序司机永远跳不出循环。Volatile在变量前加上它就是对编译器下达了最高指令不要对这个变量做任何缓存优化不要自作聪明每次用到它必须老老实实去真实的物理内存地址给我读取内存屏障_asm_volatile(dmb);//数据内存屏障CPU硬件的自作聪明现在的高级处理器内部有复杂的流水线和缓冲机制。为了追求极致的速度CPU有时会打乱代码执行的顺序乱序执行举例1.往一块内存里准备好了大量的数据2.你往控制寄存器里写了一个1命令DMA(直接内存访问控制器也就是硬件搬运工)开始把数据搬到网卡发送出去如果CPU开启了优化它可能先执行了第2步启动硬件然后再慢吞吞地把第一步的数据写入内存。结果DMA搬走的全部是错误数据乱码。_asm_:告诉C编译器括号里的字符串是汇编指令不是C语言请你原封不动地把它翻译成机械码volatile:这里放在回避那指令的后面作用是防止代码位置被优化dmbData memory Barrier)的作用:它就像是在代码中间竖起的一度坚不可摧的墙volatile(“dmb”);是一句嵌入的汇编指令。她严厉地警告CPU”在这堵墙之前的所有内存读写操作必须全部真真实实地落地完成才能开始执行这堵墙之后的指令绝不允许跨界乱序“总结asmvolatile(“dmb”);就是一堵墙意思就是以这堵墙为分界线必须要等这堵墙上面的内存读写完毕才能对这堵墙下面的内容进行操作练习1.编写环形缓冲区用处想象一下你在用串口接收数据。硬件接收数据的速度很快而且是补丁时的可能突然发来一长串。但是你的CPU主程序可能正在忙别的事情来不及立刻处理这些数据。如果不是缓冲区新来的数据就会把老数据覆盖掉导致丢包。如果是普通的数据当作缓冲区存满之后你就得帮整个数组往前挪极其耗费CPU算力环形缓冲区本质上还是普通的一维数组但是我们在逻辑上把它首尾相连当成一个“圆环”。我们需要两个指针写指针记录下一个存入数据的位置读指针记录下一个读取数据的位置#include stdio.h #include stdbool.h #include stdint.h #define BUFFER_SIZE 256//定义环形数组的大小必须是2的次方 #define BUFFER_MASK (BUFFER_SIZE-1) typedef struct { uint8_t buffer[BUFFER_SIZE];//这里用uint8_t是为了模仿硬件层面“一字节一字节”地接收和发送数据 uint16_t head;//索引用uint16,是因为uint16的最大值是65535甚至可以用uint32 uint16_t tail; }RingBuffer_t; //1.初始化缓冲区 void RingBuffer_Init(RingBuffer_t* rb) { rb-head 0; rb-tail 0; } //2.判断缓冲区是否为空 bool RingBuffer_IsEmpty(RingBuffer_t *rb) { return (rb-head rb-tail); } //3.判断缓冲区是否为满 bool RingBuffer_IsFull(RingBuffer_t* rb) { uint16_t next_head (rb-head 1) BUFFER_MASK;//任何超过BUFFER_MASK的高位都会被砍掉只保留最低的8位且速度比除法快很多 return (next_head rb-tail); } //4.存入一个字节的数据 bool RingBuffer_Write(RingBuffer_t *rb,uint8_t data) { if (RingBuffer_IsFull(rb)) { return false; } rb-buffer[rb-head] data; rb-head (rb-head 1) BUFFER_MASK; return true; } //5.读出一个字节的数据 bool RingBuffer_Read(RingBuffer_t* rb, uint8_t* data) { if (RingBuffer_IsEmpty(rb)) { return false; } *data rb-buffer[rb-tail]; rb-tail (rb-tail 1) BUFFER_MASK; return true; }2.自定义内存管理器内存池malloc和free会导致两个问题1.内存碎片化由于申请的空间大小不规律导致内存里全是七零八落的小空隙可能剩余的总的内存很大但你想要一块连续的较大字节空间时系统找不到了直接崩溃2.时间不确定性malloc寻找合适空闲内存块的时间是不确定的。有时候很快有时候要找半天。在要求“必须在1ms内做出反应”的航空或医疗设备中这是致命的解决方案既然随意大小的分配会产生碎片那我就只允许固定大小的分配核心思想1.系统一开机我们就申请一大块连续的内存比如一个大数组2.把这块大内存切成大小完全相同的N个小块比如每个块固定32字节一共切出8块3.任何人来要内存不管要多少直接扔给他一个完整的32字节的块。要是还回去我就把这块收回来4.绝对不会产生碎片且分配速度块如闪电#includestdio.h#includestdbool.h#includestdint.h#defineBLOCK_SIZE32//每个内存块的大小#defineBLOCK_COUNT8//内存块的数量typedefstruct{//内存仓库uint8_tmemory_pool[BLOCK_COUNT][BLOCK_SIZE];//一个8位的变量,它的每一位代表上面一个块的状态uint8_tbitmap;}MemoryPool_t;MemoryPool_t my_pool;//初始化函数voidMemory_Init(void){my_pool.bitmap0;//初始化状态8个全是0代表上面每一个块都空闲}//申请一个固定大小的内存块void*Memory_Alloc(void){for(inti0;iBLOCK_COUNT;i){if(((my_pool.bitmapi)1U)0){//找到的空闲块标记为占用my_pool.bitmap|(1U1);//返回这个空闲内存块的地址returnmy_pool.memory_pool[i];}}//如果内存块全满了返回NULLreturnNULL;}//释放之前申请的内存块归还给内存池voidMemory_Free(void*ptr){if(ptrNULL){return;}//将ptr强转成uint8_t*类型进行后续计算uint8_t*byte_ptr(uint8_t*)ptr;//找到内存池的起始地址uint8_t*base_ptrmy_pool.memory_pool[0][0];//算出当前地址距离内存池的起始地址多少字节除于块大小得出所处是第几块intindex(byte_ptr-base_ptr)/BLOCK_SIZE;//判断块序号是否合法if(index0indexBLOCK_SIZE){//将bitmap的第index位清0标记为空闲归还给系统my_pool.bitmap~(1Uindex);}}3.printf重定向在电脑上运行C语言中的Printf函数这句话会打印在黑色的控制台窗口里。这是因为电脑系统的标准输出默认就是显示器但是单片机就是一块芯片它没有屏幕没有控制台。如果直接调用printf系统会懵掉不懂你要显示在哪里结果往往就是程序卡死重定向的意思就是“偷梁换柱”拦截printf的底层输出告诉它别去找什么屏幕了把格式化好的每一个字符都通过硬件的串口URTA一根根线发给我的电脑底层原理不管是哪种标准的C语言标准库printf内部都做好了非常复杂的字符串拼接、数字转字符把%d变成数字等工作。但不管上面怎么折腾当它最终要把字符一个一个发送出去的时候都会统一调用最底层的一个基础函数在Keil(MDK)环境下这个最底层的函数叫fputc在GCC环境下这个最底层的函数叫_write标准库里的这个基础函数是一个“弱函数weak)”只要我们在自己的代码里重写了这个函数编译器就会用我们写的而抛弃系统默认的。#includestdio.h//假设底层硬件提供的通过串口发送1个字节的函数//如果用的是STM32HAL库这个动作可以替换为HAL_UART_Transmit)externvoidUART_SendByte(uint8_tbyte);//重写C标准库的底层的字符输出函数//以后你在代码里每次调用printf(abc);//printf在底层就会连续3次调用这个fput分别把‘a、’b、‘c当作参数传进来intfputc(intch,FILE*f){//1.我们拦截到了要打印的字符ch//2.强行把它塞进硬件串口发送出去UART_SendByte((Uint8_t)ch);//3.按照C标准必须返回刚刚发送出去的字符returnch;}//只要写了在这段代码你以后在程序里就可以肆无忌惮地使用printf如果使用的是GCC编译器比如STM32CubeIDE你想需要重写的是_write函数逻辑是一模一样的int——writeintfile,char*ptr,intlen){for(inti0;ilen;i){UART_SendByte(ptr[i]);//连续发送一串字符}returnlen;}
深度掌握C语言的重点模块
深度掌握C语言的重点模块1.位操作与寄存器编程位运算符|按位或OR有1则为1。常用于置1按位与AND全1才为1有0则为0。常用于清0或掩码~按位 取反NOT)0变11变0.^按位异或XOR相同为0不同为1。常用于翻转左移:将二进制整体向左移动右边补0。比如13就是把0001变成1000也就是十进制的8。配置寄存器的操作操作特定位铁律改变你想要改变的位绝不能影响其他位假设我们有一个8位的寄存器REG0b10100101,我们要操作它的第n位从右往左数最右边是第0位1.将特定位置1Set Bit公式REG | (1U n)原理1UU是Unsigned的意思 n会生成一个只有第n位是1其它全是0的数。将它与REG进行“或”运算根据“有1则1”的原则REG的第n位必然变成1而其它位因为和0进行或运算保持原样2.将特定位清0Clear Bit)公式REG ~1U n原理1.1U n:先生成只有第n位为1的数2.~(1U n):取反得到一个只有第n位是0其它全是1的数掩码3.把这个数跟REG进行“与”运算。根据“有0则0”的原则第n位必然变成0而其他位因为和1进行与运算保持原样。3.将特定位翻转(Toggle Bit)公式REG ^ (1U n)原理利用异或的特性。某一位与1异或原来的1会变成0原来的0会变成1指针强转与寄存器映射代码#defineGPIoA_ODR(*(volatileuint32_t*)0X40020014)假设你查阅芯片的数据手册手册上说“GPIOA的输出数据寄存器地址在0X40020014它是一个32位的寄存器。”编译器只认识代码不知道什么是地址我们要用C语言去读写这个具体的物理地址。1.0x40020014仅仅是一个十六进制的整数2.uint32_t*)0x40020014使用强制类型转换告诉编译器“请把这个数字当成一个内存指针它指向一个32位的无符号整数数据”3.volatile uint32_t*)0x40020014加上volatile告诉编译器“这个地址里的值受硬件控制随时会变千万别做缓存优化每次都要老老实实去这个地址读写”4.**(volatile unit32_t*)0x40020014在最前面加个*解引用运算符意思是“获取这个指针指向的具体内容/空间”。5.用#define GPIOA_ODR给它起个好记的名字这就完成了映射以后你在代码里写GPIOA_ODR 0xFF;C语言就会精准地向内存地址0x40020014写入0xFF,从而控制真实的硬件引脚补充说明给寄存器的某一位写1–拉高Set/High–引脚输出3.3V电压相当于提高电源正极给寄存器的某一位写0–拉低Clear/Low–引脚输出0V电压先当与链接电源负极/地练习代码#defineGPIOA_ODR(*(valatileuniit32_t*)0x40020014)#defineSET_PIN(pin)(GPIOA_ODR|(1Upin))#defineCLEAR_PIN(pin)(GPIOA_ODR~(1Upin))点亮GPIOA的第5号引脚的LED:SET_PIN(5);编译展开成(*(volatileunit32_t*)0x40020014)|(1U5);这样硬件电路检测到寄存器第5位变成1输出高电平LED亮起其他引脚状态完全不受影响熄灭GPIOA的第5号引脚的LED:CLEAR_PIN(5);编译展开成(*(volatileunit32_t*)0x40020014)~(1Upin)这样硬件电路检测到寄存器第5位变成0输出低电平LED熄灭其他引脚状态完全不受影响2.结构体位域与寄存器映射为什么学习寄存器位域假设我们要配置一个GPIO引脚它不仅仅有“高/低”电平它的配置寄存器可能长这样一个32位的今存器被拆成了多个小段第0-1位控制模式00输入01输出10复用…第2位控制输出类型0推挽1开漏第3-4位控制输出速度00低速01中速…如果用上面的方法想把模式设为01速度设为01代码要这样写REG(REG~0x1D)|(0x010)|(0x013);(REG ~0x1D)意思是先把REG要修改的区域进行擦除变成0(0x01 0)和 (0x01 3) 意思是逐步用擦除后的板子进行修改成我们想要的样子但是这个方法有个缺陷过于冗长不容易看懂而且非常容易算错位移的数量所以我们要让代码“说人话”C语言提供了一个高级武器——位域Bit-field位域typedefstruct{unit32_tMODER:2;unit32_tOTYPER:1;unit32_tOSPEEDR:2;}GPIO;在普通的C语言结构体中声明一个unit32_t就会直接占用32个bit4个字节。但是在变量名后面加上冒号和数字例如2它告诉编译器:虽然我声明的是32位的数据类型但MODER这个变量只需要占用2个bit的空间OTYPER占用1个bitOSPEEDR占用2个bit“。编译器非常聪明他就会把这些变量像拼积木一样紧紧地打包在一起内存的第0-1位被分配给了MODER内存的第2位被分配给了OTYPER内存的第3-4位被分配给了OSPEEDR这就是位域寄存器映射结构体定义好了怎么让它和真实的物理地址挂钩呢假设GPIO的端口地址是0x40020000,//1.将物理地址强转为指向该结构体的指针GPIO*gpio(GPIO*)0x40020000;//2.直接向操作面向对象里的属性一样去配置硬件gpio-MODER1;//设置为输出模式底层自动变成01gpio-OTYPER0;//设置为推挽输出底层自动变成0gpio-OSPEEDR1;//设置为中速底层自动变成01当编译器看到gpio-OSPEEDR1;gpio-OTYPER0;gpio-OSPEEDR1时那些繁琐的|~运算并没有消失它们只是被编译器接管了。但是你不用自己手动去写了它会按照你的要求帮你生成你想要修改成的样子注意当你要写入的数所占用的位数大于所分配的位数那么就只会写入低位总结优点极大提高了代码的可读性和可维护性。需要注意的坑C语言标准并没有规定位域在内存中是从左往右排还是从右往左排这取决于编译器和大小端模式。因此跨平台移植含有位域的代码时需要特别小心。不过在特定的单片机开发环境如ARM GCC STM32下排列顺序是确定的。3.函数指针与回调机制函数指针函数指针里面存的是一段代码函数在内存中的起始地址。有了函数指针函数就不再是只能被“写死”调用的 死板东西了它可以像普通变量一样被赋值、被当作参数传递、被存进数组里。typedefvoid(*ISR_Callback)(void);ISR_Callback timer_callbacks[10];typedef:意思是我们要给某种复杂的数据类型起一个“别名”左边的void:规定看这个指针只能指向没有返回值的函数*ISR_Callback):这里的ISR_Callback这就是我们起的新名字。 *表示它是一个指针。右边的void规定了这个指针只能指向不需要传递参数的函数ISR_Callback timer_callbacks[10];这个是一个ISR_Callback类型的数组回调机制Callback)有这样一个场景单片机里有一个硬件定时器Timer。时间一到硬件就会强行打断CPU去执行一个底层写死的函数中断服务函数普通的写法强耦合直接在底层的中断函数里写业务逻辑voidHardware_Timer_INtertupt(void){LED_Toggle();//翻转LED灯}缺点如果有一天需求变了”定时器时间到了不闪灯了改为控制电机转动。“你必须要去修改底层的驱动代码底层驱动和上层应用深深地绑死在了一起强耦合代码根本无法复用高手的写法回调机制解耦底层驱动维护数组。它不知道时间到了该干嘛它只负责“查字典”。voidHardware_Timer_Interrupt(void){//检查第0个坑位有没有登记函数如果有就执行它if(timer_callbacks[0]NULL){timer_callbacks[0]();}}上层的应用代码可以随便改voidMy_Motor_Control(void){//控制电机转动的代码...}intmain(){//注册回调函数timer_callbacks[0]My_Motor_Control;while(1){}}我只要把函数指针数组对应位置的地址改成我要执行的函数地址就可以做到“解耦”4.volatile与内存屏障Volatileunit32_t*status_reg;_asm_volatile(dmb);//数据内存屏障volatile和内存屏障这两个概念存在的原因是编译器和CPU太聪明了聪明到有时会“自作主张”假设你写了这样一段等待硬件准备就绪的代码unit32_t*status_reg(unit32_t*)0x40001000;//假设这是硬件寄存器while(*status_reg0){//苦苦等待硬件把这个寄存器变成1}printf(硬件准备好了);这时我们聪明的C编译器看到死循环里没有任何代码修改*status_reg的值他就觉得每次都去物理内存里面读取太慢太傻了就自作主张把第一次读到的值缓存到CPU内部超快的寄存器里面以后一直判断缓存的值。这样我们后面硬件在1ms后悄悄把物理内存里的位置的值改变了但是CPU还在比对的是寄存器里面的值旧数据就会导致程序司机永远跳不出循环。Volatile在变量前加上它就是对编译器下达了最高指令不要对这个变量做任何缓存优化不要自作聪明每次用到它必须老老实实去真实的物理内存地址给我读取内存屏障_asm_volatile(dmb);//数据内存屏障CPU硬件的自作聪明现在的高级处理器内部有复杂的流水线和缓冲机制。为了追求极致的速度CPU有时会打乱代码执行的顺序乱序执行举例1.往一块内存里准备好了大量的数据2.你往控制寄存器里写了一个1命令DMA(直接内存访问控制器也就是硬件搬运工)开始把数据搬到网卡发送出去如果CPU开启了优化它可能先执行了第2步启动硬件然后再慢吞吞地把第一步的数据写入内存。结果DMA搬走的全部是错误数据乱码。_asm_:告诉C编译器括号里的字符串是汇编指令不是C语言请你原封不动地把它翻译成机械码volatile:这里放在回避那指令的后面作用是防止代码位置被优化dmbData memory Barrier)的作用:它就像是在代码中间竖起的一度坚不可摧的墙volatile(“dmb”);是一句嵌入的汇编指令。她严厉地警告CPU”在这堵墙之前的所有内存读写操作必须全部真真实实地落地完成才能开始执行这堵墙之后的指令绝不允许跨界乱序“总结asmvolatile(“dmb”);就是一堵墙意思就是以这堵墙为分界线必须要等这堵墙上面的内存读写完毕才能对这堵墙下面的内容进行操作练习1.编写环形缓冲区用处想象一下你在用串口接收数据。硬件接收数据的速度很快而且是补丁时的可能突然发来一长串。但是你的CPU主程序可能正在忙别的事情来不及立刻处理这些数据。如果不是缓冲区新来的数据就会把老数据覆盖掉导致丢包。如果是普通的数据当作缓冲区存满之后你就得帮整个数组往前挪极其耗费CPU算力环形缓冲区本质上还是普通的一维数组但是我们在逻辑上把它首尾相连当成一个“圆环”。我们需要两个指针写指针记录下一个存入数据的位置读指针记录下一个读取数据的位置#include stdio.h #include stdbool.h #include stdint.h #define BUFFER_SIZE 256//定义环形数组的大小必须是2的次方 #define BUFFER_MASK (BUFFER_SIZE-1) typedef struct { uint8_t buffer[BUFFER_SIZE];//这里用uint8_t是为了模仿硬件层面“一字节一字节”地接收和发送数据 uint16_t head;//索引用uint16,是因为uint16的最大值是65535甚至可以用uint32 uint16_t tail; }RingBuffer_t; //1.初始化缓冲区 void RingBuffer_Init(RingBuffer_t* rb) { rb-head 0; rb-tail 0; } //2.判断缓冲区是否为空 bool RingBuffer_IsEmpty(RingBuffer_t *rb) { return (rb-head rb-tail); } //3.判断缓冲区是否为满 bool RingBuffer_IsFull(RingBuffer_t* rb) { uint16_t next_head (rb-head 1) BUFFER_MASK;//任何超过BUFFER_MASK的高位都会被砍掉只保留最低的8位且速度比除法快很多 return (next_head rb-tail); } //4.存入一个字节的数据 bool RingBuffer_Write(RingBuffer_t *rb,uint8_t data) { if (RingBuffer_IsFull(rb)) { return false; } rb-buffer[rb-head] data; rb-head (rb-head 1) BUFFER_MASK; return true; } //5.读出一个字节的数据 bool RingBuffer_Read(RingBuffer_t* rb, uint8_t* data) { if (RingBuffer_IsEmpty(rb)) { return false; } *data rb-buffer[rb-tail]; rb-tail (rb-tail 1) BUFFER_MASK; return true; }2.自定义内存管理器内存池malloc和free会导致两个问题1.内存碎片化由于申请的空间大小不规律导致内存里全是七零八落的小空隙可能剩余的总的内存很大但你想要一块连续的较大字节空间时系统找不到了直接崩溃2.时间不确定性malloc寻找合适空闲内存块的时间是不确定的。有时候很快有时候要找半天。在要求“必须在1ms内做出反应”的航空或医疗设备中这是致命的解决方案既然随意大小的分配会产生碎片那我就只允许固定大小的分配核心思想1.系统一开机我们就申请一大块连续的内存比如一个大数组2.把这块大内存切成大小完全相同的N个小块比如每个块固定32字节一共切出8块3.任何人来要内存不管要多少直接扔给他一个完整的32字节的块。要是还回去我就把这块收回来4.绝对不会产生碎片且分配速度块如闪电#includestdio.h#includestdbool.h#includestdint.h#defineBLOCK_SIZE32//每个内存块的大小#defineBLOCK_COUNT8//内存块的数量typedefstruct{//内存仓库uint8_tmemory_pool[BLOCK_COUNT][BLOCK_SIZE];//一个8位的变量,它的每一位代表上面一个块的状态uint8_tbitmap;}MemoryPool_t;MemoryPool_t my_pool;//初始化函数voidMemory_Init(void){my_pool.bitmap0;//初始化状态8个全是0代表上面每一个块都空闲}//申请一个固定大小的内存块void*Memory_Alloc(void){for(inti0;iBLOCK_COUNT;i){if(((my_pool.bitmapi)1U)0){//找到的空闲块标记为占用my_pool.bitmap|(1U1);//返回这个空闲内存块的地址returnmy_pool.memory_pool[i];}}//如果内存块全满了返回NULLreturnNULL;}//释放之前申请的内存块归还给内存池voidMemory_Free(void*ptr){if(ptrNULL){return;}//将ptr强转成uint8_t*类型进行后续计算uint8_t*byte_ptr(uint8_t*)ptr;//找到内存池的起始地址uint8_t*base_ptrmy_pool.memory_pool[0][0];//算出当前地址距离内存池的起始地址多少字节除于块大小得出所处是第几块intindex(byte_ptr-base_ptr)/BLOCK_SIZE;//判断块序号是否合法if(index0indexBLOCK_SIZE){//将bitmap的第index位清0标记为空闲归还给系统my_pool.bitmap~(1Uindex);}}3.printf重定向在电脑上运行C语言中的Printf函数这句话会打印在黑色的控制台窗口里。这是因为电脑系统的标准输出默认就是显示器但是单片机就是一块芯片它没有屏幕没有控制台。如果直接调用printf系统会懵掉不懂你要显示在哪里结果往往就是程序卡死重定向的意思就是“偷梁换柱”拦截printf的底层输出告诉它别去找什么屏幕了把格式化好的每一个字符都通过硬件的串口URTA一根根线发给我的电脑底层原理不管是哪种标准的C语言标准库printf内部都做好了非常复杂的字符串拼接、数字转字符把%d变成数字等工作。但不管上面怎么折腾当它最终要把字符一个一个发送出去的时候都会统一调用最底层的一个基础函数在Keil(MDK)环境下这个最底层的函数叫fputc在GCC环境下这个最底层的函数叫_write标准库里的这个基础函数是一个“弱函数weak)”只要我们在自己的代码里重写了这个函数编译器就会用我们写的而抛弃系统默认的。#includestdio.h//假设底层硬件提供的通过串口发送1个字节的函数//如果用的是STM32HAL库这个动作可以替换为HAL_UART_Transmit)externvoidUART_SendByte(uint8_tbyte);//重写C标准库的底层的字符输出函数//以后你在代码里每次调用printf(abc);//printf在底层就会连续3次调用这个fput分别把‘a、’b、‘c当作参数传进来intfputc(intch,FILE*f){//1.我们拦截到了要打印的字符ch//2.强行把它塞进硬件串口发送出去UART_SendByte((Uint8_t)ch);//3.按照C标准必须返回刚刚发送出去的字符returnch;}//只要写了在这段代码你以后在程序里就可以肆无忌惮地使用printf如果使用的是GCC编译器比如STM32CubeIDE你想需要重写的是_write函数逻辑是一模一样的int——writeintfile,char*ptr,intlen){for(inti0;ilen;i){UART_SendByte(ptr[i]);//连续发送一串字符}returnlen;}