(嵌入式面经)第11章 20+公司面经杂谈(六):字节跳动、小米、算能科技、格力、恒玄科技

本篇涉及的所有问题概要:大家可以试试在看参考答案前,提前尝试解答,以便明确自身知识点的不足部分!

1.串口没有时钟线,是怎么工作的,而为什么SPI需要时钟线?

2.有没有用过printf打印这个函数,它的底层是如何实现的?

3.如何保证线程间的正确性?

4.Linux中字符设备与块设备的主要区别是什么?

5.NAND Flash 与 NOR Flash 的区别是什么?

6.主设备号和次设备号的用途是什么?

7.串口波特率计算,115200下,数据吞吐量一般为多少(1S传输多少字节的数据)?

8.在程序中你是如何接收并解析大数据包(例如1K字节以上)的?

9.在 FreeRTOS 中,哪些事件会触发任务调度?

10.专栏订阅奖励(支持模仿)——个人创新点问答:在你的FreeRTOS PLUS中是如何实现支持不同场景(工作(空闲任务)、空闲(定时唤醒))下自适应低功耗休眠策略的?

---------------------------------------------------------------------------------------------------

1.串口没有时钟线,是怎么工作的,而为什么SPI需要时钟线?

在串口通信中,虽然没有专用的时钟线,但通信仍然能够正常进行。

其原因是串口(通常是UART,即通用异步收发传输器)采用的是 异步通信 模式。以下是具体的工作原理和实现方式:

1. 异步通信的工作原理

在异步串口通信中,数据的传输依赖于双方事先约定好的波特率(Baud Rate)。

波特率是通信中数据传输的速率,它决定了每秒钟传输多少位数据。

通过设定相同的波特率,发送方和接收方就能在同样的时间间隔内收发数据。

2. 如何保证时序同步?

在没有时钟线的情况下,通信双方是如何保持时序同步的呢?

数据格式

  • 在串口通信中,数据是通过一定格式进行传输的。
  • 每个数据单元通常由一个 起始位数据位、可选的 校验位停止位 组成。
  • 起始位用来标识数据传输的开始,而停止位用来标识数据的结束。
  • 这使得接收方能够准确地知道何时开始和结束读取数据。

波特率

  • 发送方和接收方都使用相同的波特率,这意味着它们在同一时间间隔内发送和接收相同数量的位(即每秒传输的比特数)。
  • 因此,只要双方在通信开始前设定了相同的波特率,就能保证数据能够同步传输。
  • 即便没有专用的时钟信号,接收方也能够根据这些预先设定好的时间间隔正确地解码收到的数据。

时钟恢复

  • 接收方通过 采样 发送的数据,通常会根据一个固定的采样频率(即波特率)来确定数据的比特值。
  • 虽然没有时钟线,但接收方会使用自己的本地时钟来从收到的信号中恢复数据位的信息。
  • 接收方将数据流中的每一位数据分为若干小段,按时钟频率的时间段进行采样,确保准确地读取数据位。

3. 为什么 SPI 必须加时钟线?

SPI(串行外设接口)与串口(UART)不同,它使用 同步通信,这意味着它需要时钟线。

SPI 需要时钟线来确保发送和接收设备能够同时在相同的时刻同步交换数据。

相比之下,串口通信通过波特率来同步数据,因此不需要时钟线。

4.代码示例:使用UART进行串行通信

以下是一个简单的使用UART进行串行通信的C语言代码示例:

初始化UARTUART_Init函数初始化UART,并设置波特率。在这个例子中,波特率被设置为9600。

发送数据:通过UART_Transmit函数将字符发送到串口。该函数会等待直到发送缓冲区为空,然后发送数据。

接收数据:通过UART_Receive函数接收一个字符。此函数会阻塞,直到接收到数据。

输出示例:假设程序发送字符 'A',接收并打印出来,输出是 :Received data: A

总结

  • 异步串口通信:不需要时钟线,通过预先设定的波特率和数据格式来确保发送方和接收方的数据同步。
  • 波特率同步:发送方和接收方使用相同的波特率进行数据传输,接收方通过本地时钟同步数据。
  • 为什么不需要时钟线:串口通信依赖于波特率同步,而不是通过外部时钟信号进行同步。虽然没有时钟线,但依然可以确保数据的正确传输。
  • SPI和串口的区别:SPI 使用时钟线,适用于同步通信;而串口使用波特率进行异步通信,不需要时钟线。
  • 2.有没有用过printf打印这个函数,它的底层是如何实现的?

    1. printf 函数介绍

    printf 是 C 语言中的标准库函数,主要用于将格式化数据输出到标准输出(通常是屏幕)。

    它的底层实现非常复杂,因为它需要处理各种格式化操作,并将数据最终输出。

    2. printf 的底层实现步骤

    printf 的工作流程大致可以分为以下几个步骤:

    解析格式字符串:

    • printf 需要解析传入的格式字符串,识别其中的格式化占位符(如 %d%s 等),并处理它们。

    参数提取和类型转换:

    • printf 使用 va_list(变长参数)来访问参数列表。
    • 通过 va_startva_arg 宏,printf 按照格式字符串的要求提取不同类型的参数。

    格式化输出:

    • 根据格式标识符(如 %d%f 等),printf 将数据转换成适当的格式。
    • 例如,整数需要转化为字符串,浮点数需要根据精度要求进行格式化。

    缓冲区管理:

    • 格式化后的数据被存入内部缓冲区。为了优化效率,数据在缓冲区中暂存,直到缓冲区满或者遇到换行符时才会被刷新,写入到标准输出。

    最终输出:

  • 当数据格式化并存入缓冲区后,最终的输出通过 write 系统调用或类似机制输出到标准输出设备(通常是终端或屏幕)。
  • 在嵌入式系统中,printf 可能通过串口或者其他通信接口输出数据。
  • 3. 为什么 printf 实现复杂?

    printf 需要处理不同类型的格式(整数、浮点数、字符串等)并进行各种格式化操作。它还需要支持变长参数,处理数据时需要考虑类型安全和对齐问题。通过使用 va_list 进行变长参数处理,printf 能够支持多种数据类型。

    4. 简化版 printf 示例

    下面是一个简化版的 printf 实现,使用 va_list 来处理变长参数:

  • va_list args;:声明一个 va_list 类型的变量,用于保存变长参数列表。
  • va_start(args, format);:初始化 va_list,使其指向格式化字符串之后的第一个变长参数。
  • va_arg(args, type);:从 args 中提取下一个参数,并根据指定的类型进行转换(如 intchar* 等)。
  • putchar(*ptr);:当当前字符不是格式标识符(如 %d%s)时,直接输出普通字符。
  • va_end(args);:结束变长参数的处理,释放资源。
  • 总结

  • 对于高性能应用,避免频繁使用 printf,因为它的底层实现可能涉及较多的内存操作和格式化计算。
  • 在嵌入式系统中,printf 可能会导致较大的内存开销,可以使用更轻量的输出方法。
  • 3.如何保证线程间的正确性?

    多线程编程中,保证线程的正确性是非常重要的,因为多个线程共享资源

    必须采取适当的同步方法来避免数据竞争死锁等问题。

    介绍几种常见的保证线程正确性的方法,并通过代码示例进行说明。

    1. 使用互斥锁(Mutex)

    互斥锁是一种常见的同步机制,它用于保护共享资源,确保同一时刻只有一个线程能够访问资源,

    从而避免多个线程同时访问共享资源时产生的竞态条件。

  • pthread_mutex_lockpthread_mutex_unlock 用于保护共享资源 shared_resource,确保同一时刻只有一个线程可以修改它。
  • 每个线程在访问共享资源之前,都需要获取锁,执行完毕后释放锁。
  • 2. 使用读写锁(Read-Write Lock)

    读写锁允许多个线程同时读取共享数据,但只有一个线程能够写共享数据,适合于读多写少的场景。

  • pthread_rwlock_rdlock 用于获取读锁,多个读线程可以同时读取数据。
  • pthread_rwlock_wrlock 用于获取写锁,确保同一时刻只有一个写线程可以修改共享数据。
  • 3. 使用条件变量(Condition Variables)

    条件变量允许线程等待某个条件成立,并且在该条件满足时通知其他线程。常与互斥锁一起使用。

  • pthread_cond_wait 让消费者线程等待条件变量,直到生产者线程发出信号。
  • pthread_cond_signal 用于生产者线程通知消费者线程继续执行。
  • 4. 使用原子操作(Atomic Operations)

    原子操作保证某些操作在多线程环境中不可分割,避免了锁的使用,通常用于简单的计数或标志位操作。

    • atomic_fetch_add_explicit 是一个原子操作,用于原子地增加 counter 的值。

    5. 避免死锁

    死锁发生在多个线程之间,互相等待对方释放锁的情况,导致所有线程无法继续执行。为避免死锁,可以:

    • 避免锁嵌套(避免在持有一个锁时请求其他锁)
    • 使用锁顺序(确保按固定顺序请求锁)
    • 使用超时机制(避免无限等待)

    总结

    为了保证线程的正确性,需要使用同步机制(如互斥锁读写锁条件变量等)来保护共享资源,防止数据竞争。

    还可以通过原子操作和线程安全的数据结构来简化编程。有效的线程管理能够提高系统的稳定性和性能。

    4.Linux中字符设备与块设备的主要区别是什么?

    在 Linux 操作系统中,字符设备和块设备是两种非常常见的硬件设备类型。

    它们在数据传输方式访问方式设备驱动管理方面有明显的不同。

    1. 字符设备与块设备的主要区别

    数据传输方式

  • 字符设备:以字符为单位进行数据传输,数据以字节流的形式按顺序处理,读写操作是实时进行的,不存在缓冲机制。这意味着字符设备通常不涉及数据的缓存,每次读取或写入都会直接操作数据。示例:键盘、鼠标、串口通信等设备。
  • 块设备:以数据块为单位传输数据,通常数据块大小为 512 字节或其倍数,支持随机访问。这类设备通常有缓冲机制,用于提高读写效率,通过缓存减少访问延迟。示例:硬盘、U盘、SSD 等存储设备。
  • 访问方式

    • 字符设备:采用顺序访问方式,数据逐字节读取或写入。字符设备不支持在设备的任意位置进行随机访问,所有的读写操作都是按顺序进行的。
    • 块设备:支持随机访问,可以在设备的任何位置读取或写入数据。块设备的驱动程序会对数据块进行优化,例如调度算法、重排序等,以提升访问性能。

    设备驱动与管理

    • 字符设备:驱动程序较为简单,主要负责字符的读写和控制操作。内核通过字符设备文件来管理字符设备。
    • 块设备:驱动程序较为复杂,需要处理缓存管理、数据块调度、错误恢复等功能。块设备的管理通常涉及更复杂的 I/O 调度器和缓存机制。

    2. 字符设备与块设备对比

    3. 代码示例

    字符设备模拟

    字符设备通过字节流方式处理数据。下面代码示例模拟了字符设备的读写操作:

    • 该代码模拟字符设备的行为,首先向文件 char_device.txt 写入字母 A 到 Z,然后读取并按字节输出这些字符。

    块设备模拟

    块设备通过固定大小的数据块(如 512 字节)进行读写。以下代码示例,模拟了一个块设备的读写操作。

    • 该代码模拟块设备的操作,通过将数据分为多个 512 字节的数据块进行写入,并读取第一个数据块并输出其前 20 字节。

    总结

    • 字符设备:数据传输按字节流处理,实时且顺序读写,适用于如键盘、鼠标等设备,驱动程序较为简单。
    • 块设备:数据传输按固定大小的数据块进行,支持随机访问,适用于硬盘、U盘等存储设备,驱动程序较为复杂,涉及缓存和调度优化。

    5.NAND Flash 与 NOR Flash 的区

    剩余60%内容,订阅专栏后可继续查看/也可单篇购买

    作者简介:仅用几个月时间0基础天坑急转嵌入式开发,逆袭成功拿下华为、vivo、小米等15个offer,面试经验100+,收藏20+面经,分享求职历程与学习心得。 专栏内容:这是一份覆盖嵌入式求职过程中99%问题指南,详细讲解了嵌入式开发的学习路径、项目经验分享、简历优化技巧、面试心得及实习经验,从技术面,HR面,AI面,主管面,谈薪一站式服务,助你突破技术瓶颈、打破信息差,争取更多大厂offer。

    全部评论

    相关推荐

    评论
    12
    24
    分享

    创作者周榜

    更多
    牛客网
    牛客企业服务