标签 c语言 下的文章

大家好,我是良许。

最近收到不少粉丝的私信,问得最多的就是:"良许,嵌入式方向这么多,我到底该选哪个?"说实话,这个问题我当年也纠结过。

今天就结合我这些年的经验,跟大家好好聊聊嵌入式就业方向的选择问题。

1. 嵌入式领域的主流方向

1.1 单片机开发方向

这是我入行时的第一个方向。

单片机开发主要是基于STM32、51单片机、PIC等芯片进行底层开发,通常应用在智能家居、工业控制、医疗设备等领域。

这个方向的特点是门槛相对较低,但要做精也不容易。

你需要掌握C语言、硬件电路知识、各种外设驱动(GPIO、UART、SPI、I2C等)。

举个例子,用STM32的HAL库点个灯看起来简单:

// 初始化GPIO
void LED_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    __HAL_RCC_GPIOA_CLK_ENABLE();
    
    GPIO_InitStruct.Pin = GPIO_PIN_5;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}

// 主循环控制LED闪烁
int main(void)
{
    HAL_Init();
    LED_Init();
    
    while(1)
    {
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
        HAL_Delay(500);
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
        HAL_Delay(500);
    }
}

但实际项目中,你要处理的是复杂的通信协议、实时性要求、功耗优化、抗干扰设计等问题。

这个方向的薪资在一线城市应届生大概8K-12K,3年经验能到15K-20K。

1.2 嵌入式Linux应用开发

这是我27岁进入外企后的主要方向,也是目前市场需求最大的方向之一。

主要工作是在Linux系统上开发应用程序,涉及文件IO、进程线程、网络编程、数据库操作等。

这个方向需要你熟悉Linux系统编程、Shell脚本、网络协议栈、多线程编程等。

比如一个简单的多线程读取传感器数据的例子:

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void* sensor_read_thread(void* arg)
{
    int sensor_id = *(int*)arg;
    while(1)
    {
        // 模拟读取传感器数据
        printf("Sensor %d: Reading data...\n", sensor_id);
        sleep(1);
    }
    return NULL;
}

int main()
{
    pthread_t thread1, thread2;
    int sensor1 = 1, sensor2 = 2;
    
    pthread_create(&thread1, NULL, sensor_read_thread, &sensor1);
    pthread_create(&thread2, NULL, sensor_read_thread, &sensor2);
    
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    
    return 0;
}

这个方向在汽车电子、智能设备、工业互联网等领域应用广泛。

薪资方面,应届生大概10K-15K,3年经验能达到20K-30K,在外企或大厂甚至更高。

1.3 嵌入式Linux驱动开发

这是嵌入式领域的高端方向,主要负责编写Linux内核驱动程序,让硬件设备能够在Linux系统上正常工作。

需要深入理解Linux内核机制、硬件原理、驱动框架等。

驱动开发的难度比较大,需要掌握内核模块编程、设备树、中断处理、DMA等知识。

一个简单的字符设备驱动框架:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>

static dev_t dev_num;
static struct cdev my_cdev;

static int my_open(struct inode *inode, struct file *file)
{
    printk("Device opened\n");
    return 0;
}

static ssize_t my_read(struct file *file, char __user *buf, 
                       size_t count, loff_t *ppos)
{
    printk("Device read\n");
    return 0;
}

static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = my_open,
    .read = my_read,
};

static int __init my_driver_init(void)
{
    alloc_chrdev_region(&dev_num, 0, 1, "my_device");
    cdev_init(&my_cdev, &fops);
    cdev_add(&my_cdev, dev_num, 1);
    return 0;
}

module_init(my_driver_init);

这个方向的薪资是嵌入式领域最高的之一,有3-5年经验的驱动工程师在一线城市能拿到25K-40K,资深的甚至能达到50K以上。

1.4 RTOS实时操作系统开发

RTOS方向主要应用在对实时性要求极高的场景,比如航空航天、医疗设备、工业控制等。常用的RTOS有FreeRTOS、RT-Thread、μC/OS等。

这个方向需要理解任务调度、信号量、消息队列、内存管理等概念。

FreeRTOS的一个简单任务创建示例:

#include "FreeRTOS.h"
#include "task.h"

void vTask1(void *pvParameters)
{
    while(1)
    {
        printf("Task 1 running\n");
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void vTask2(void *pvParameters)
{
    while(1)
    {
        printf("Task 2 running\n");
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

int main(void)
{
    xTaskCreate(vTask1, "Task1", 128, NULL, 1, NULL);
    xTaskCreate(vTask2, "Task2", 128, NULL, 2, NULL);
    
    vTaskStartScheduler();
    
    while(1);
}

RTOS方向的薪资水平介于单片机和Linux之间,应届生大概9K-13K,3年经验能到18K-25K。

1.5 汽车电子方向

这是我目前深耕的领域。

汽车电子包括ADAS(高级驾驶辅助系统)、车载娱乐系统、动力系统控制等。

需要了解AUTOSAR架构、CAN/LIN总线、车规级开发流程等。

汽车电子对可靠性和安全性要求极高,需要遵循ISO 26262等功能安全标准。

这个方向的技术栈比较综合,既要懂硬件,又要懂软件,还要了解汽车行业的特殊要求。

薪资方面,汽车电子在传统车企可能不算特别高,但在新能源车企和Tier1供应商,待遇还是很不错的。

应届生大概10K-14K,3年经验能达到20K-30K,资深工程师35K以上。

2. 如何选择适合自己的方向

2.1 根据兴趣和特长选择

如果你喜欢硬件,动手能力强,喜欢焊电路板、调试硬件,那单片机或RTOS方向可能更适合你。

如果你更喜欢软件编程,喜欢研究算法和系统架构,那Linux应用或驱动开发会是更好的选择。

我当年选择Linux方向,就是因为发现自己更擅长软件编程,对底层原理也很感兴趣。

虽然本科学的是机械,但编程让我找到了真正的兴趣所在。

2.2 考虑市场需求和发展前景

从市场需求来看,目前嵌入式Linux开发的岗位最多,尤其是在物联网、智能设备、汽车电子等领域。

单片机开发虽然岗位也不少,但相对来说技术含量和薪资天花板会低一些。

驱动开发岗位相对较少,但薪资高,竞争也激烈。

RTOS方向比较小众,但在特定领域(如航空航天、医疗设备)有不可替代的地位。

汽车电子是近几年的热门方向,随着新能源汽车和智能驾驶的发展,这个领域的需求还在持续增长。

如果你看好汽车行业的未来,这是个不错的选择。

2.3 评估学习难度和时间成本

单片机开发入门相对容易,几个月的学习就能上手做项目。

Linux应用开发需要半年到一年的系统学习。

驱动开发难度最大,可能需要1-2年的深入学习和实践。

我的建议是,如果你是应届生或转行新手,可以先从单片机或Linux应用入手,积累一定经验后再考虑往更深的方向发展。

我自己就是这样走过来的,先做单片机,再做Linux应用,现在也在不断学习驱动相关的知识。

2.4 考虑地域因素

不同城市对不同方向的需求也不一样。

北京、上海、深圳、杭州等一线城市,各个方向的岗位都比较多。

但如果你在二三线城市,可能单片机和工业控制方向的岗位会更多一些。

我在福州,这边汽车电子和工业控制的公司比较多,所以我选择深耕汽车电子方向。

你也要结合自己所在城市或打算去的城市来考虑。

3. 我的一些建议

3.1 不要过早限制自己

很多人一开始就想选定一个方向,然后一直做下去。

但实际上,嵌入式的各个方向是相通的,底层的C语言、数据结构、操作系统原理都是共通的。

我的经历就是最好的例子。

虽然我现在主要做Linux应用开发,但单片机的经验让我对硬件有更深的理解,这在做Linux开发时也很有帮助。

所以不要害怕尝试不同的方向,每一段经历都是财富。

3.2 重视基础知识的学习

无论选择哪个方向,C语言、数据结构、操作系统原理、计算机网络这些基础知识都是必须掌握的。

很多人急于学习具体的技术,却忽视了基础,这样后期的发展会受限。

我在写公众号的过程中,发现很多读者的问题其实都是基础不扎实导致的。

所以我一直强调,要花时间把基础打牢,这比学习具体的技术更重要。

3.3 多做项目,积累经验

理论学习固然重要,但嵌入式是一个实践性很强的领域。

你需要通过做项目来巩固知识,发现问题,解决问题。

可以从简单的项目开始,比如用STM32做一个温湿度监测系统,用树莓派做一个智能家居控制器。

然后逐步增加难度,做一些综合性的项目。

我当年就是通过做各种小项目,慢慢积累起来的。

3.4 关注行业动态,持续学习

嵌入式领域的技术更新很快,新的芯片、新的操作系统、新的开发工具层出不穷。

你需要保持学习的习惯,关注行业动态,了解新技术的发展。

我现在每天都会花时间看技术文章、学习新知识。虽然工作很忙,但学习不能停。

这也是我为什么要做公众号的原因之一,通过写作来倒逼自己学习,同时也能帮助更多的人。

3.5 建立自己的技术体系

随着经验的积累,你需要建立自己的技术体系,形成自己的技术壁垒。

这不仅仅是掌握某个具体的技术,而是要有系统的思维,能够解决复杂的问题。

比如我现在做嵌入式Linux开发,不仅要会写应用程序,还要了解底层驱动、内核机制、硬件原理。

这样在遇到问题时,我能够从多个角度去分析和解决。

这种综合能力是需要长期积累的。

4. 写在最后

嵌入式就业方向的选择,没有绝对的好坏,只有适合不适合。

关键是要了解自己的兴趣和特长,结合市场需求和发展前景,做出适合自己的选择。

我从机械转到嵌入式,从单片机做到Linux,从打工到创业,这一路走来,最大的感悟就是:选择很重要,但更重要的是选择之后的坚持和努力。

无论你选择哪个方向,只要用心去做,都能做出成绩。

希望这篇文章能给你一些启发和帮助。

如果你还有其他问题,欢迎在评论区留言,我会尽力解答。

也欢迎关注我的公众号,我会持续分享嵌入式相关的技术文章和经验心得。

最后,祝大家都能找到适合自己的方向,在嵌入式领域走得更远!

更多编程学习资源

大家好,我是良许。

今天我们来聊一聊 C 语言中最让初学者头疼,却又最强大的特性——指针。

作为一名从事嵌入式开发多年的程序员,我深知指针在底层编程中的重要性。

无论是操作硬件寄存器、管理动态内存,还是实现高效的数据结构,指针都扮演着不可或缺的角色。

1. 什么是指针

1.1 指针的本质

指针其实就是一个变量,只不过这个变量存储的不是普通的数值,而是内存地址。

我们可以把内存想象成一排排的房间,每个房间都有一个门牌号(地址),而指针就是记录这个门牌号的本子。

通过这个门牌号,我们可以找到对应的房间,进而访问或修改房间里的内容。

在嵌入式开发中,这个概念尤为重要。比如 STM32 的 GPIO 端口,其实就是通过固定的内存地址来访问的。

当我们要点亮一个 LED 灯时,本质上就是通过指针操作特定地址的寄存器。

1.2 为什么需要指针

指针的存在主要解决了以下几个问题:

第一,高效传递数据。

当我们需要在函数之间传递大型数据结构时,如果直接传递整个结构体,会产生大量的复制开销。

而使用指针,只需要传递一个地址(通常是 4 字节或 8 字节),效率大大提升。

第二,动态内存管理。

在嵌入式系统中,内存资源往往非常有限。

通过指针和动态内存分配,我们可以在程序运行时根据实际需要申请和释放内存,提高内存利用率。

第三,直接操作硬件。

在嵌入式开发中,我们经常需要直接访问硬件寄存器。

这些寄存器都有固定的物理地址,必须通过指针来访问。

2. 指针的基本使用

2.1 指针的声明和初始化

声明一个指针变量的语法是在类型名后面加上星号(*)。例如:

int *p;        // 声明一个指向整型的指针
char *str;     // 声明一个指向字符的指针
float *fp;     // 声明一个指向浮点数的指针

需要注意的是,刚声明的指针是野指针,它指向一个不确定的地址,使用前必须初始化。

我们可以用取地址符(&)来获取变量的地址:

int num = 100;
int *p = &num;  // p指向num的地址
​
printf("num的值: %d\n", num);
printf("num的地址: %p\n", &num);
printf("p存储的地址: %p\n", p);
printf("p指向的值: %d\n", *p);

这段代码会输出 num 的值、num 的地址、指针 p 存储的地址(与 num 的地址相同),以及通过指针 p 访问到的值(也是 100)。

2.2 指针的解引用

解引用就是通过指针访问它所指向的内存中的值。

使用星号(*)操作符可以实现解引用:

int a = 50;
int *ptr = &a;
​
printf("a的值: %d\n", a);        // 输出50
printf("*ptr的值: %d\n", *ptr);  // 输出50
​
*ptr = 80;  // 通过指针修改a的值
printf("修改后a的值: %d\n", a);  // 输出80

在这个例子中,我们通过指针 ptr 修改了变量 a 的值。

这在函数参数传递中非常有用,可以实现真正的"传址调用"。

2.3 指针与函数

在 C 语言中,函数参数默认是值传递,也就是说函数内部对参数的修改不会影响外部变量。

但通过指针,我们可以实现传址调用:

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}
​
int main(void) {
    int x = 10, y = 20;
    printf("交换前: x=%d, y=%d\n", x, y);
    
    swap(&x, &y);
    printf("交换后: x=%d, y=%d\n", x, y);
    
    return 0;
}

这个经典的交换函数例子展示了指针的威力。

通过传递变量的地址,函数内部可以直接修改外部变量的值。

3. 指针的进阶应用

3.1 指针与数组

数组名本身就是一个指针常量,指向数组的首元素。

这是 C 语言中一个非常重要的概念:

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;  // 等价于 int *p = &arr[0];
​
printf("arr[0] = %d\n", arr[0]);    // 输出1
printf("*p = %d\n", *p);            // 输出1
printf("*(p+1) = %d\n", *(p+1));    // 输出2
printf("p[2] = %d\n", p[2]);        // 输出3

指针可以进行算术运算。

当指针加 1 时,实际上是移动了一个所指类型的大小。

比如 int 类型占 4 字节,那么 p+1 实际上是地址增加 4。

在嵌入式开发中,这个特性经常用于遍历数据缓冲区:

uint8_t buffer[256];
uint8_t *ptr = buffer;
​
// 通过指针遍历整个缓冲区
for(int i = 0; i < 256; i++) {
    *ptr = i;  // 写入数据
    ptr++;     // 指针移动到下一个位置
}

3.2 指针与字符串

在 C 语言中,字符串实际上就是字符数组,而字符串的操作大量使用指针:

char str[] = "Hello";
char *p = str;
​
while(*p != '\0') {
    printf("%c", *p);
    p++;
}
printf("\n");

这段代码通过指针遍历字符串并逐个打印字符。

在实际开发中,我们经常需要处理字符串,比如解析串口接收到的 AT 指令:

void parse_at_command(char *cmd) {
    if(strncmp(cmd, "AT+", 3) == 0) {
        char *param = cmd + 3;  // 指针偏移到参数部分
        printf("收到AT指令,参数: %s\n", param);
    }
}

3.3 多级指针

指针本身也是变量,也有自己的地址,因此可以有指向指针的指针,称为多级指针:

int num = 100;
int *p = &num;      // 一级指针
int **pp = &p;      // 二级指针

printf("num = %d\n", num);
printf("*p = %d\n", *p);
printf("**pp = %d\n", **pp);

**pp = 200;  // 通过二级指针修改num的值
printf("修改后num = %d\n", num);

多级指针在动态二维数组、函数指针数组等场景中很常见。

在嵌入式开发中,有时需要动态管理设备列表,就会用到二级指针。

4. 指针在嵌入式中的实战应用

4.1 操作硬件寄存器

在 STM32 开发中,我们经常需要直接操作寄存器。

这些寄存器都有固定的物理地址,必须通过指针访问:

// 定义GPIO端口的基地址
#define GPIOA_BASE    0x40020000U
#define GPIOA_MODER   (*(volatile uint32_t *)(GPIOA_BASE + 0x00))
#define GPIOA_ODR     (*(volatile uint32_t *)(GPIOA_BASE + 0x14))

// 配置PA5为输出模式
void led_init(void) {
    // 使能GPIOA时钟
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

    // 配置PA5为输出模式
    GPIOA_MODER &= ~(3U << (5 * 2));  // 清除原配置
    GPIOA_MODER |= (1U << (5 * 2));   // 设置为输出
}

// 点亮LED
void led_on(void) {
    GPIOA_ODR |= (1U << 5);
}

// 熄灭LED
void led_off(void) {
    GPIOA_ODR &= ~(1U << 5);
}

这里的 volatile 关键字非常重要,它告诉编译器这个变量可能被外部因素改变,不要对其进行优化。

在访问硬件寄存器时必须使用 volatile 修饰。

4.2 DMA 数据传输

在使用 STM32 的 DMA 功能时,我们需要指定源地址和目标地址,这都是通过指针实现的:

uint8_t tx_buffer[128];
uint8_t rx_buffer[128];

void dma_uart_init(void) {
    // 配置DMA
    hdma_usart1_tx.Instance = DMA2_Stream7;
    hdma_usart1_tx.Init.Channel = DMA_CHANNEL_4;
    hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
    hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;

    HAL_DMA_Init(&hdma_usart1_tx);
}

void send_data_via_dma(void) {
    // 通过DMA发送数据,传递缓冲区指针
    HAL_UART_Transmit_DMA(&huart1, tx_buffer, sizeof(tx_buffer));
}

4.3 动态内存管理

在嵌入式系统中,虽然要谨慎使用动态内存,但在某些场景下确实需要:

#include <stdlib.h>

typedef struct {
    uint8_t id;
    uint16_t data;
    uint32_t timestamp;
} sensor_data_t;

sensor_data_t* create_sensor_data(uint8_t id) {
    sensor_data_t *data = (sensor_data_t*)malloc(sizeof(sensor_data_t));
    if(data != NULL) {
        data->id = id;
        data->data = 0;
        data->timestamp = HAL_GetTick();
    }
    return data;
}

void process_sensor(void) {
    sensor_data_t *sensor = create_sensor_data(1);
    if(sensor != NULL) {
        // 处理传感器数据
        sensor->data = read_sensor();

        // 使用完毕后释放内存
        free(sensor);
    }
}

需要注意的是,在嵌入式系统中使用动态内存要特别小心,因为频繁的 malloc 和 free 可能导致内存碎片,影响系统稳定性。

5. 指针使用的注意事项

5.1 野指针问题

野指针是指向未知内存区域的指针,使用野指针会导致程序崩溃或产生不可预测的行为:

int *p;  // 野指针,未初始化
*p = 10; // 危险!可能导致程序崩溃

// 正确做法
int *p = NULL;  // 初始化为NULL
if(p != NULL) {
    *p = 10;
}

在使用指针前,一定要确保它已经被正确初始化。

养成将指针初始化为 NULL 的习惯,并在使用前检查是否为 NULL。

5.2 内存泄漏

动态分配的内存如果忘记释放,就会造成内存泄漏:

void memory_leak_example(void) {
    int *p = (int*)malloc(sizeof(int) * 100);
    // 使用p
    // 忘记调用free(p),造成内存泄漏
}

// 正确做法
void correct_example(void) {
    int *p = (int*)malloc(sizeof(int) * 100);
    if(p != NULL) {
        // 使用p
        free(p);
        p = NULL;  // 释放后置为NULL
    }
}

5.3 悬空指针

当指针指向的内存被释放后,如果继续使用该指针,就会产生悬空指针问题:

int *p = (int*)malloc(sizeof(int));
*p = 100;
free(p);
// p现在是悬空指针
*p = 200;  // 危险!访问已释放的内存

// 正确做法
free(p);
p = NULL;  // 释放后立即置为NULL

6. 总结

指针是 C 语言的精髓,也是嵌入式开发的基石。

虽然初学时可能觉得难以理解,但只要多加练习,理解其本质(就是内存地址),就能逐渐掌握。

在我多年的嵌入式开发经验中,指针无处不在:从操作硬件寄存器到管理数据结构,从函数参数传递到实现复杂算法,都离不开指针。

掌握指针不仅能让你写出更高效的代码,还能帮助你深入理解计算机的工作原理。

特别是在嵌入式领域,对指针的熟练运用直接关系到能否写出高质量的底层代码。

希望这篇文章能帮助大家更好地理解和使用 C 语言的指针,在嵌入式开发的道路上走得更远。

开始学c语言。有一起学的欢迎加我交流,对于c语言,我基本是个门外汉,没接触过,没办法了,