首页 / 资讯中心 / 文章详情

嵌入式C语言函数入门:从定义到模块化实战应用

嵌入式C语言函数入门:从定义到模块化实战应用 ★ FEATURED ARTICLE
学嵌入式第11天今天把C语言函数啃了一遍。讲真一开始觉得“函数不就是把代码包一下嘛”直到我试着用函数重构LED闪烁和按键扫描后才发现之前十天的代码有多乱。这篇日志就整理一下我从函数定义到实际应用的过程包括踩过的坑、看过的资料以及一个能直接上手的按键控制LED示例。适合刚开始学嵌入式或者电子专业的同学参考核心目标是用函数写出可维护、可扩展的裸机代码。1. 为什么函数在嵌入式里不是可选项而是必选项1.1 嵌入式开发的代码最终要比你预想的大得多很多初学单片机的朋友会有一个错觉点个灯、读个按键几十行代码就完了函数好像没有存在的必要。我前几天的状态也是这样main函数里塞满while循环GPIO初始化、延时、按键去抖全堆在一起功能确实能跑但一旦开始加定时器、串口、传感器整个代码就像一团乱麻。比如我之前写一个按键控制LED闪烁的功能所有逻辑都在main的while里改动一个延时时间要找半天。等到第10天我想在同一个工程里加串口打印时发现main函数已经超过两百行。那会儿我才真正理解为什么有些全栈嵌入式工程师反复强调“写代码不是给机器看的是给人看的”。函数就是把一块块逻辑包装成可以命名、可以复用的积木让main函数变得像阅读说明书一样清晰。嵌入式系统和普通PC程序最大的不同是资源紧张但代码结构不能因此就乱。尤其是跑裸机开发的场景没有操作系统帮你隔离任务越是用函数把不同功能边界划清楚后续调试和扩充就越省力。1.2 函数是结构化编程的基石也是嵌入式架构师的起点如果往长远想从单片机小项目一路做到嵌入式Linux、做产品级固件函数意识是最基础的那层台阶。架构师画模块框图时每个小方框最终映射到代码里就是一个或几个函数模块之间的箭头就是函数调用关系。要是连函数参数、返回值都设计得混乱架构再漂亮也落不了地。所以在Day11这个节点我没有直接去追求复杂的指针、链表先把函数吃透。这其实也是我给自己定下的嵌入式学习路线里重要的一环C语言语法基础、函数与模块化、指针与内存、结构与链表、状态机与嵌入式常用算法。函数如果不过关后面学中断服务程序、学FreeRTOS任务回调都会很吃力。2. 函数的基本定义与调用2.1 函数四要素返回类型、函数名、参数列表、函数体C语言函数的标准写法非常简单但每个部分都有讲究。一个完整的函数由四部分组成返回类型 函数名(参数列表) { 函数体; return 返回值; }比如我写的第一个实用函数void delay_ms(unsigned int ms) { unsigned int i, j; for (i 0; i ms; i) { for (j 0; j 120; j); } }这个函数返回类型是void表示不返回任何数据函数名是delay_ms参数列表里有一个unsigned int类型的ms表示要延时多少毫秒函数体就是一个空循环。为什么要设计返回类型因为有些函数执行完需要告诉调用者结果。比如写一个读取温度传感器的函数返回温度值就是float类型判断按键是否被按下返回布尔值或整数。返回类型选错了轻则计算结果不对重则导致隐式类型转换产生警告在嵌入式编译环境下可能被当成错误处理。参数列表里也要特别注意类型。嵌入式平台上int可能占2字节或4字节为了明确宽度我习惯用uint8_t、uint16_t、uint32_t这些stdint.h里定义的类型。举个例子如果函数参数表示计数次数用uint8_t最多只能表达255次计数器一多就溢出不仔细看你可能根本发现不了。2.2 函数声明与定义的区别搞不清会报错这是初学者最容易栽跟头的地方过热词里也常出现“函数定义”“函数声明”。简单说定义是函数的完整实现它创建了函数本身声明只是告诉编译器“有这个函数它的返回类型和参数长这样”并不包含函数体。如果在一个文件里在调用函数之前先写了完整的函数定义那么不需要单独声明。就像我早期玩51单片机时一样把所有函数都写在main前面一切正常。但工程变大后函数被分散到不同的.c文件里调用其他文件的函数前必须做声明。先看一个典型的错误场景#include stdio.h int main(void) { int result add(3, 5); // 编译器不知道add是什么 printf(%d\n, result); return 0; } int add(int a, int b) { return a b; }在GCC编译器下这个代码可能会报警告说add隐式声明在一些严格编译选项下直接编译失败。解决办法是在文件开头加一句int add(int a, int b);这就像告诉别人“我认识一个叫add的函数它接收两个int返回int”后面真正实现放在哪里都可以反正调用点认识它了。但在嵌入式工程里我更推荐用头文件进行声明。比如led.h里写#ifndef __LED_H #define __LED_H void led_init(void); void led_on(void); void led_off(void); #endifled.c里写对应的定义其他文件只要#include led.h就可以调用了。这比在每个文件里手动敲函数声明要规范得多改一处签名只需改头文件不至于到处漏改。2.3 值传递与地址传递嵌入式函数躲不开的细节C语言函数参数默认是值传递也就是把实参的值复制一份给形参函数内修改形参不会影响外面的变量。对于普通变量确实如此但嵌入式开发中大量使用数组、指针、结构体这时就需要用地址传递。举一个我最开始写错的例子想通过函数修改全局标志位结果怎么改都没生效。void change_flag(int flag) { flag 1; // 只会改变形参的备份 }正确的做法是传入指针void change_flag(int *flag) { *flag 1; }调用时用取地址change_flag(sysFlag)。在嵌入式里直接操作寄存器和硬件地址本质也是通过指针完成的。比如通用的位带操作、串口发送数据函数形参往往会写成指针或数组名。数组名本身就会退化为首地址所以向函数传数组时写法是int *arr或int arr[]效果一样。要特别注意指针传参时有可能会造成越界修改。比如写一个数组求和函数我习惯同时传入数组长度防止访问越界。这就是为什么很多嵌入式库里函数签名像这样void log_data(uint8_t *data, uint16_t len);既给了数据起始地址也给了数据个数函数在处理时就能做边界检查这也是安全固件设计里比较基础的意识。3. 函数在模块化与可维护性上的实战价值3.1 从裸机while堆代码到分层设计的转变学嵌入式C语言我觉得最关键的一个转变就是“把功能拆开”。市面上很多入门教程和开源项目会把代码分成BSP板级支持包、HAL硬件抽象层、APP应用层这一套东西落到笔头上其实就是函数的分布问题。最开始我给自己写的板级初始化是这样的void board_init(void) { GPIO_InitTypeDef gpioInit; // 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 配置引脚为推挽输出 gpioInit.GPIO_Pin GPIO_Pin_5; gpioInit.GPIO_Mode GPIO_Mode_Out_PP; gpioInit.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpioInit); }这个函数负责把所有外设引脚和时钟都准备好。随后LED函数、按键函数、串口函数都各自独立main函数里看起来就很清晰int main(void) { board_init(); led_init(); key_init(); uart_init(); while (1) { key_scan(); led_toggle(); } }这种编码方式的优势只有改功能的时候才体会得到。比如要把LED从PA5换到PB0只要去led.c里修改引脚的配置main函数一行都不用动。如果是以前那种全堆在main里的写法改个引脚配置文件不小心就会牵连别的功能。3.2 用函数封装一个完整的LED控制模块今天下午我重新写了一个LED模块目标是让所有和LED相关的操作都封装在led.c中外部只能调用几个简单接口。这里放一下我的实现思路。led.h里对外暴露三个接口#ifndef __LED_H #define __LED_H #include stm32f10x.h void LED_Init(void); void LED_On(void); void LED_Off(void); void LED_Toggle(void); #endifled.c里做的具体实现#include led.h #define LED_GPIO_PORT GPIOA #define LED_PIN GPIO_Pin_5 #define LED_CLK_ENABLE RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE) void LED_Init(void) { GPIO_InitTypeDef gpio; LED_CLK_ENABLE; gpio.GPIO_Pin LED_PIN; gpio.GPIO_Mode GPIO_Mode_Out_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(LED_GPIO_PORT, gpio); } void LED_On(void) { GPIO_SetBits(LED_GPIO_PORT, LED_PIN); } void LED_Off(void) { GPIO_ResetBits(LED_GPIO_PORT, LED_PIN); } void LED_Toggle(void) { if (GPIO_ReadOutputDataBit(LED_GPIO_PORT, LED_PIN) Bit_SET) { GPIO_ResetBits(LED_GPIO_PORT, LED_PIN); } else { GPIO_SetBits(LED_GPIO_PORT, LED_PIN); } }这样做的直接好处是整个工程里不会出现第二处操作LED引脚的地方。我要做LED闪烁只需要在main里调用LED_Toggle()配合延时要做呼吸灯也只要改写这一个模块的内部实现对外接口不变。对代码阅读者来说看到LED_On就知道这是开灯不用去关心寄存器细节。3.3 把算法封装成独立函数例如冒泡排序函数不仅用于控制硬件算法逻辑同样应该以函数为单位存在。热词里有人搜“冒泡排序c语言”说明很多同学都在写这种排序练习。我之前觉得很无聊现在才发现写排序函数的过程是理解参数传递和返回值的绝佳训练。比如这样一个数组冒泡排序函数void bubble_sort(uint16_t arr[], uint16_t n) { uint16_t i, j; uint16_t temp; for (i 0; i n - 1; i) { for (j 0; j n - i - 1; j) { if (arr[j] arr[j 1]) { temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; } } } }这个函数接收数组和长度直接修改原数组元素顺序因为数组名传的是首地址。调用时写bubble_sort(data, sizeof(data)/sizeof(data[0]))。注意在函数内部无法通过sizeof(arr)得到数组长度因为形参已经变成了指针这也是很多初学者想当然的地方。从这个例子还能看出设计函数时要把“算法”和“数据”分开。排序算法本身不关心数组里是什么含义只要给出比较方式和交换规则就能排序。这样通用性高以后处理不同长度的数据都可以复用同一个函数。3.4 头文件与源文件的组织工程化第一步让函数真正发挥威力离不开合理的文件组织。我见过一些初学者把几十个函数堆在一个main.c里那只能叫“结构化”不能叫“模块化”。真正的模块化应该一个模块一个.c文件配套一个.h文件。以前写51单片机时用Keil一个main.c写完所有代码后来转到STM32的标准库工程看到官方把每个外设封装成独立文件才恍然大悟。现在我自己练习就沿用这套风格例如user/ ├── main.c ├── bsp/ │ ├── bsp_led.c │ └── bsp_led.h ├── bsp/ │ ├── bsp_key.c │ └── bsp_key.h └── bsp/ ├── bsp_uart.c └── bsp_uart.h每个模块的.c文件里做定义.h文件做声明和宏。这样别人接手你的项目看目录结构就能猜到有哪些功能找代码也快。对于将来参加嵌入式开源项目或者进入团队协作这种习惯甚至比懂几个高深语法更重要。要注意头文件里不只放函数声明还应该放必要的宏定义、结构体定义。但是尽量避免在头文件里定义变量一旦被多个.c文件包含容易造成重复定义。正确的做法是头文件里用extern声明变量在某个.c文件里做定义。这个细节我循环在“symbol multiply defined”错误上踩过很多次。4. 嵌入式函数的高级应用函数指针与回调机制4.1 函数指针的声明与调用学完基本函数我开始接触嵌入式里非常实用的一个概念——函数指针。函数名在表达式里会退化为函数首地址因此可以把函数地址存到指针变量里再通过指针调用函数。声明方式看起来有点绕比如int add(int a, int b); int (*func_ptr)(int, int); // 一个指向函数的指针 func_ptr add; int result func_ptr(3, 4);刚开始总是记不住“*”和“()”的优先级。这里有个记忆技巧看清标识符与谁先结合。如果写成int *func_ptr(int, int)那么func_ptr先和()结合说明它是一个函数返回int指针如果写成int (func_ptr)(int, int)括号让func_ptr先和结合所以是一个指针指向一个函数。在嵌入式代码里函数指针最常见的用途就是实现回调函数、维护函数表、实现状态机跳转。比如一款芯片的启动代码中中断向量表本质上就是一个函数指针数组C标准库里的qsort也靠传入函数指针来决定排序规则。4.2 用回调函数驱动按键处理回调机制简单说就是我把一个函数的地址传给你你合适的时候调用它。这种思想在实时嵌入式场景里极其常见。我写一个按键扫描模块就用了函数指针来注册事件。按键模块负责轮询GPIO电平、处理消抖但它并不知道某一次“按键按下”事件具体要做什么于是对外提供一个注册接口typedef void (*key_callback_t)(void); static key_callback_t key_press_cb 0; void key_register_callback(key_callback_t cb) { key_press_cb cb; } void key_scan(void) { // 扫描并消抖 if (key_press_cb ! 0) { key_press_cb(); } }在main.c里我只需要注册一次void on_key_pressed(void); int main(void) { key_init(); key_register_callback(on_key_pressed); while (1) { key_scan(); } } void on_key_pressed(void) { LED_Toggle(); }以后按键按下要做的事情从“LED_Toggle”换成“蜂鸣器响”main函数里只改注册那一行的函数名按键模块完全不需要懂应用逻辑。这种设计思路在网络热词里对应着“回调函数”在更大一点的嵌入式系统、协议栈、GUI框架中几乎是标配。4.3 中断服务函数的特殊性别把ISR当普通函数用嵌入式开发里还有一种特殊的函数叫中断服务函数也就是ISRInterrupt Service Routine。和使用普通函数不同ISR不由main里其他函数调用而是由硬件事件触发比如定时器溢出、串口接收、外部引脚跳变都会跳入对应的中断函数。在Cortex-M内核单片机里中断函数名需要跟启动文件里的向量表名称保持一致类型通常是void、无参数void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 更新计数 } }中断函数里有一些非常需要注意的禁忌。第一ISR要尽量短不要在中断里做延时、阻塞等待、执行长时间算法否则会耽误下一次中断响应严重时主程序完全卡死。第二如果ISR里和main函数共享某些变量需要将这些变量声明为volatile防止编译器优化时main函数读取不到最新值。第三如果有复杂的后台处理需求ISR里可以设置一个标志位main函数轮询这个标志位再去处理。很多刚入门的朋友喜欢在中断里打印调试信息结果串口波特率稍高一点整个系统就变得极不稳定。一方面因为printf往往很耗时另一方面中断里频繁访问外设还可能造成重入。这样一层层排查下来最终还是要回到模块化设计思路上来把ISR当做一个最精简的入口函数。5. 函数设计中的作用域、生命周期与常见错误5.1 全局变量和局部变量的选择影响的是系统资源在嵌入式C语言里变量按位置分为全局变量、局部变量按存储类型又可以分为普通的auto、static、register、extern等。局部变量定义在函数内部生命周期从函数被调用开始到函数返回结束全局变量定义在函数外部整个程序运行期间一直存在。很多人爱用全局变量觉得传参数麻烦。但全局变量过多会让各函数之间的耦合度急剧上升你改一个变量值很可能在别处引发错误。比如我之前的工程里有一个全局delay_cnt多个中断和主循环都改它结果逻辑混乱查了很久才发现是变量在别处被覆盖了。局部变量则在函数的栈空间中分配。嵌入式单片机RAM本来就小如果在函数里定义一个很大的局部数组栈可能不够用导致栈溢出程序跑飞。因此大型缓冲区通常用静态数组放在源文件里或者定义成全局数组。需要注意静态局部变量用static修饰后存储在静态区即使离开函数值也不会丢失这很适合用来记录上次状态。5.2 数据的隐藏与保护static关键字妙用static是C语言里多功能的关键字在函数设计中尤其重要。用它修饰函数可以限制函数只在当前源文件内可见防止其他文件调用起到“模块内部助手函数”的作用。我以前写整个工程时会把内部延时函数都暴露在.h里后来发现根本没必要外部模块不需要知道LED是怎么延时的。于是把类似bsp_led.c里的一些辅助函数用static修饰这样即便别的文件包含同名函数也不会因为全局符号冲突而报错。尤其是在多个工程师协作或使用第三方库时static能让命名空间干净不少。同样的道理也适用于全局变量。如果一个变量只是模块内部使用比如记录按键状态的press_state就可以在.c文件里定义成static再通过接口函数读写外部模块就不能随意破坏这个状态。5.3 编译链接错误undefined reference与重复定义嵌入式C语言编译过程可以分为预处理、编译、汇编、链接多个阶段。函数定义缺失、声明不匹配都会在链接阶段暴露问题。这里列举几个我常遇到的现象也给大家一个排查思路。第一种编译时报“undefined reference tofunc”。这说明编译器没找到函数实现。可能原因是忘记把包含该函数定义的.c文件加入工程函数名拼写不一致函数声明和定义不在同一作用域下。请回头检查.c文件的编译选项、头文件里的函数原型是否一致。第二种报“multiply defined symbol”说明同一个函数名在多个.c文件里都有定义。常见场景是函数定义不小心写在了.h文件里而且没有用内联或static修饰每个包含该头文件的源文件都会生成一份副本链接时就冲突了。解决办法是把定义挪到.c文件头文件只放声明。第三种报“implicit declaration of function”常见于忘记#include头文件或者函数声明放在调用点之后。现在很多编译器把它当错误处理因为隐式声明默认函数返回int这对嵌入式开发有风险。5.4 经典避坑看门狗、中断与可重入函数在实时系统里还有一个概念叫可重入。可重入函数可以被多个任务或中断调用而不会产生数据错乱。比如在中断中调用一个函数而这个函数也同时被主循环调用如果它修改了一个共享的静态变量就会相互干扰。让函数可重入有几个原则不要使用static局部变量来临时存储状态不要修改全局变量除非通过互斥机制不要调用不可重入的函数比如某些C标准库的rand函数有隐藏状态。嵌入式开源项目里很多开发者会把打印、延时、协议解析写成可重入或者设计成带参数的函数从根上防止并发问题。另一个嵌入式特有的坑是看门狗。如果函数内部有长时间循环比如自写的delay_ms延时几十毫秒在不让出CPU的情况下就可能来不及喂狗导致系统复位。所以要学会把阻塞式的耗时操作拆成短暂的分段操作最好直接利用定时器或操作系统延时函数。6. 今日练手一个函数化完整的按键控制LED工程6.1 完整代码结构与实现流程光说不练假把式把我今天写的小工程分享出来。它实现的功能是按键长按点亮LED松开熄灭串口打印按键状态和LED状态。使用标准外设库平台为STM32F103C8T6。整个工程只有三个主要模块main.c负责调用和注册bsp_led.c负责LEDbsp_key.c负责按键扫描和回调。先看main.c#include bsp_led.h #include bsp_key.h #include bsp_uart.h void key_pressed_action(void); void key_released_action(void); int main(void) { LED_Init(); UART_Init(115200); Key_Init(); Key_RegisterCallback(key_pressed_action, key_released_action); printf(System started.\r\n); while (1) { Key_Scan(); LED_Toggle(); delay_ms(100); } } void key_pressed_action(void) { LED_On(); printf(Key pressed\r\n); } void key_released_action(void) { LED_Off(); printf(Key released\r\n); }bsp_key.h#ifndef __BSP_KEY_H #define __BSP_KEY_H #include stm32f10x.h typedef void (*key_callback_t)(void); void Key_Init(void); void Key_Scan(void); void Key_RegisterCallback(key_callback_t press_cb, key_callback_t release_cb); #endifbsp_key.c的核心扫描部分如下#include bsp_key.h static key_callback_t press_handler 0; static key_callback_t release_handler 0; static uint8_t key_state 1; // 1表示未按下0表示按下 static uint8_t key_last_state 1; static uint16_t key_debounce_cnt 0; static uint8_t key_pressed_flag 0; static uint8_t key_released_flag 0; void Key_Init(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); gpio.GPIO_Pin GPIO_Pin_0; gpio.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, gpio); } void Key_RegisterCallback(key_callback_t press_cb, key_callback_t release_cb) { press_handler press_cb; release_handler release_cb; } void Key_Scan(void) { uint8_t level GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0); if (level 0) { if (key_last_state 1) { key_debounce_cnt; if (key_debounce_cnt 5) { key_state 0; key_pressed_flag 1; key_debounce_cnt 0; } } } else { if (key_last_state 0) { key_debounce_cnt; if (key_debounce_cnt 5) { key_state 1; key_released_flag 1; key_debounce_cnt 0; } } } key_last_state level; if (key_pressed_flag) { key_pressed_flag 0; if (press_handler) press_handler(); } if (key_released_flag) { key_released_flag 0; if (release_handler) release_handler(); } }虽然这个扫描函数是轮询模式但函数已经把所有按键逻辑封装起来了。main函数里没有直接检查GPIO电平和消抖看到Key_Scan()就明白它负责扫描按键看到注册的回调就明白按键动作会怎样影响其他模块。相比一开始把所有逻辑堆在一起代码阅读成本大幅度降低。6.2 这个案例教会我的其实是“拆”字写完这个工程我最大的感受是函数入门的精髓不在数学技巧而在“拆”的能力。把一个复杂的应用拆成若干个职责单一的模块每个模块再拆成若干个小函数。函数的功能越小可复用性越高测试起来也越容易。当然拆也不是越碎越好。如果函数颗粒度太小可能为了一个两行的操作就跳转一次函数在性能敏感的嵌入式场景下会有影响。好在现代编译器一般会做内联优化我们更应优先保证代码结构清晰。等以后真正用RTOS、做Bootloader、移植开源协议栈就会更感谢当初把函数基础打牢的自己。6.3 关于编译器、工具链的一点提醒热词里有人搜到“pnpm无法识别”“make无法识别”之类的错误这通常是在Windows命令行下环境没有配置好。我以前也遇到过直接在CMD里输入嵌入式交叉编译器命令结果提示“不是内部或外部命令”。这类问题往往不是函数本身的问题但会阻碍我们编译验证。解决办法很简单把编译器目录添加到系统环境变量PATH或者使用IDE自带的控制台例如Keil的Command窗口、VS Code的集成终端。对于嵌入式学习我个人觉得用MDK、IAR这类IDE入门更省心编译、下载、调试一条龙。等熟练以后再尝试用CMake或Makefile构建工程、用GCC编译链做实验那时候你对编译链接的理解会更深一层。7. 学习日志之外的几点实用心得7.1 学会阅读开源代码里的函数现在的嵌入式学习资源非常丰富包括各种嵌入式开源项目热词里也常看到“嵌入式开源项目”被搜到。但很多人下载代码后不知道怎么学习我建议先抓住函数清单。打开一个工程时不急着看main先看所有.h文件里声明了哪些函数每个函数结合名字猜它的用途。再看每个函数的实现重点观察输入参数是什么、返回什么、函数内部动作是否需要外部状态配合。用这种思路读代码半小时就能摸清一个模块的大致逻辑。我前天读一个串口协议库就是用函数先勾出“发送、接收、解析、打包”四条主线然后再深入细节。7.2 建立自己的函数库与代码风格随着学习深入我已经开始积累自己的基础函数库比如延时、IO操作、按键扫描、串口printf重定向、LED呼吸灯等。每写一个新的平台驱动我都会把通用逻辑尽量抽象出来再通过函数参数和宏配置区别不同硬件。时间久了做一个新项目时很多模块就不需要重新编写。代码风格方面我给自己定了几条简单规则函数名用“模块_动作”的命名方式比如UART_Send、UART_Receive每个函数都有注释说明功能、参数、返回值和注意事项一个函数尽量控制在50行以内超过就考虑重构。这些规则不需要很复杂但长期坚持下来阅读自己老代码时会非常舒服。7.3 用调试手段来验证函数不要凭空猜也许你在调试函数时遇到过这样的情况明明函数写得正确运行时就是不按预想执行。除了检查逻辑我建议熟练使用调试器。在Keil或IAR环境中打断点看函数调用栈单步执行查看局部变量的值。通过调试才能分辨出是参数传错、寄存器配置不对还是被中断意外抢占了。对于使用STM32的朋友可以打开HardFault_Handler调试查看处理器寄存器。如果是在某一个函数调用后进入HardFault优先排查数组越界、访问空指针、栈溢出等情况。函数写多了很多memory corruption问题往往隐藏在这些细节里。今天从函数定义讲到声明、参数传递再到模块化设计、函数指针和回调、ISR禁忌最后还跑了一个按键控制LED的完整工程。这一通操作下来我才算是把“从定义到应用”走通了。函数本身并不难难的是建立拆分逻辑、命名规范、边界防护的习惯。猜你如果正在学嵌入式应该也和当初的我一样想要一个清晰又不做作的总结那我就把这些经历原原本本写下来希望能帮你少走一点弯路。
阅读完成 · 觉得有帮助?
咨询建站