Try   HackMD

韌體工程師 面試

考題

1. 位元操作

給一個整數變數 a,寫出兩段程式碼。第一個要設定 a 的 bit 4,第二個要清除 a 的 bit 4。在以上兩個操作中,要保持其它位不變。

#define SET_BIT(a, n) ((a) |= (1 << (n)))
#define CLEAR_BIT(a, n) ((a) &= ~(1 << (n)))

2. 中斷(interrupt)

典型的,這個新的key word是__interrupt。以下的程式碼使用__interrupt來定義一個中斷service routine。評論這個程式碼:

__interrupt double compute_area(double radius)
{
    double area = PI*radius*radius;
    printf("\nArea=%f", area);
    return area;
}

這個函數有太多錯誤了,問題如下:

  • ISR 不能返回一個值
  • ISR 不能傳遞參數
  • 在許多編譯器/處理器中,浮點數操作是不可重入的(re-entrant)。有些處理器/編譯器需要讓多餘的暫存器入棧(PUSH入堆疊),有些處理器/編譯器就是不允許在 ISR 中做浮點運算。此外,ISR應該是短而有效率的,在 ISR 中做浮點運算是不明智的。
  • 與第三點類似,printf 通常會有可重入和效能的問題。

3. 0xFFFF 和 ~0 的差異

評論以下的程式碼片段?

unsigned int zero = 0;
unsigned int compzero = 0xFFFF;
/* 1's complement of zero */
對於一個 int 型不是 16 位元的機器來說,0xFFFF 將會導致錯誤。

unsigned int compzero = 0xFFFF;

  • 是一個 16 位元的十六進位數值,對應的二進位表示是 1111 1111 1111 1111
  • 在 unsigned int(通常是 32 位元)中,這個值會被擴展為 0x0000FFFF(即 0000 0000 0000 0000 1111 1111 1111 1111)。

unsigned int compzero = ~0;

  • 0 的二進位表示是 0000 0000 0000 0000 0000 0000 0000 0000(假設 unsigned int 為 32 位元)。
  • ~0 代表對 0 取反,變成 1111 1111 1111 1111 1111 1111 1111 1111,即 0xFFFFFFFF

正確的程式碼如下:

unsigned int compzero = ~0;

這個問題真的可以知道應試者是否了解字組長度在電腦的重要性。在我的經驗中,好的嵌入式程式設計師會嚴謹的知道硬體的細節與它的限制,然後電腦程式設計師傾向忽視硬體並把它視為一個必要的煩擾。

相關題目

What Is The Output Of this program?

#include <stdio.h> 

int main()     
{ 
    unsigned int a = 0xffff; 
    unsigned int k = ~a; 
    printf("%d %d\n", a, k); 
    return 0; 
} 

Answer
Output: 65535 -65536

Explaination:
a0x0000ffff,因此 k0xffff0000。而 %d 代表要印出 signed int%u 才是印出 unsigned int

4. 解釋 “struct” 和 “union”

特性 struct(結構體) union(聯合體)
記憶體分配 每個成員都有自己的儲存空間,總大小為所有成員大小的總和(加上對齊填充) 所有成員共用相同的記憶體空間,大小等於最大成員的大小
成員存取 可以同時存取多個成員 只能存取一個成員,寫入一個成員會覆蓋其他成員
用途 用來組織不同類型的相關資料 用來節省記憶體,適合不同情況下存不同的值

struct 在 C 上是一種資料型態裡面可以包含多種資料型態,並且會按照程式中定義時擺放的順序在記憶體中排序,按照宣告的順序擺放。編譯器會自動為 struct 的成員分配空間。

在 C++ 上 struct 可以包含方法,和 class 幾乎相同,只是繼承與存取權限預設皆為 public (相反地 class 權限預設為 private)。另外,class 比 struct 更常用於泛型程式設計。例如:

template <typename T>
class MyClass {
    T value;
public:
    MyClass(T v) : value(v) {}
    T getValue() { return value; }
};

int main() {
    MyClass<int> obj(10);
    printf("%d\n", obj.getValue());  // 10
    return 0;
}
什麼是泛型程式設計?

泛型程式設計(Generic Programming) 是一種程式設計範式,它允許你編寫可以適用於不同資料型別的程式碼,而不需要針對每種型別重複撰寫相同的程式碼。
在 C++ 中,泛型程式設計最常透過 Template 來實作。

假設你要寫一個函式來交換兩個變數的值:

void swap_int(int &a, int &b) {
    int temp = a;
    a = b;
    b = temp;
}

這函式只能交換 int 型別,如果要交換 doublechar,就必須額外寫:

void swap_double(double &a, double &b) { ... }
void swap_char(char &a, char &b) { ... }

這樣的程式碼會變得重複且難以維護。
泛型程式設計 讓我們可以寫一個通用的函式,適用於所有型別:

template <typename T>
void swap_generic(T &a, T &b) {
    T temp = a;
    a = b;
    b = temp;
}

這樣我們就可以用同一個 swap_generic 函式交換不同型別:

int x = 10, y = 20;
double a = 1.1, b = 2.2;
swap_generic(x, y);
swap_generic(a, b);

union 是一種特殊的資料型態,所有內容都從記憶體開頭開始存取的資料型態,並且他的大小由內部最大的那個資料型態來定義。會用同一個存儲空間,只能存儲最後一個成員訊息。只給其中一個成員給值而使用其他值就會壞掉。

範例

#include <stdio.h>
#include <string.h>

struct MyStruct {
    int a;
    float b;
    char c[10];
};

union MyUnion {
    int a;
    float b;
    char c[10];
};

int main() {
    struct MyStruct s = {1, 2.5, "Hello"};
    union MyUnion u;

    // 設定 union 的不同成員
    u.a = 1;
    printf("Union a: %d\n", u.a);

    u.b = 2.5;  // 這會覆蓋 `a`
    printf("Union b: %f\n", u.b);
    printf("Union a (被覆蓋後): %d\n", u.a);  // 這時候 `a` 的值已經被破壞

    strcpy(u.c, "Hello");  // 這會覆蓋 `b`
    printf("Union c: %s\n", u.c);
    printf("Union b (被覆蓋後): %f\n", u.b);  // `b` 的值不再正確

    return 0;
}

執行結果
可以看到 union 的成員共用記憶體,因此寫入 b 之後,a 的值就變了。

Union a: 1
Union b: 2.500000
Union a (被覆蓋後): 1082130432
Union c: Hello
Union b (被覆蓋後): 1261528175.000000
補充:enum

C 語言提供自定型態為 enum,是一組由識別字所代表的整數常數(不可變更其值)。除非特別指定,不然都是由 0 開始,接下來遞增 1。例如:

enum week{Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday};

5. Reverse Linked List

給你一個指向一組 Linked List 的指標叫做 head,請你將這組Linked List 進行反轉,並且回傳反轉後的 Linked List。

class Solution {
public:
    struct ListNode* reverseList(ListNode* head) {
        
        struct ListNode* newhead = NULL;
        struct ListNode* curr = head;
        while (curr != NULL){
            struct ListNode* next = curr->next;
            curr->next = newhead;
            newhead = curr;
            curr = next;
        }
        return newhead;

    }
};

空間複雜度為 O(1),時間複雜度為 O(n)

6. call by value / call by pointer / call by reference

  • call by value 傳值
  • call by pointer 傳址
  • call by reference 傳參考
  • *: pointer, dereference(解指標)
  • &: address-of
  • **: pointer to pointer(指標的指標)

call by value

caller 和 callee 各自有自己的記憶體。
這種參數傳遞方式是最簡單的方式,就是把每個參數都複製一份到函式裡運算。

int add(int x, int y) {
    return x + y;
}

這種方式在某些情形變得不適用,例如要修改傳遞進來的參數,或者不想複製一份參數浪費效能。這時就會採用傳址 call by pointer 或傳參考 call by reference。

call by pointer

又稱 Call by address,但為了跟後面的方式作區分,用 Call by pointer 這名子比較明確,這邊以最簡單的swap作為範例。

void swap(int *x, int *y) {
    int tmp = *x;
    *x = *y;
    *y = tmp;
}

int a = 3;
int b = 5;
swap(&a, &b);

call by reference

caller 和 callee使用相同的記憶體,C++ 才有。
好處在於之後在函式裡的變數寫法如同一般的變數寫法。

void swap(int &x, int &y) {
    int tmp = x;
    x = y;
    y = tmp;
}

int a = 3;
int b = 5;
swap(a, b);

7. Implement a Queue and Stack by linked-list

沒看清楚題目要問甚麼?但可能是要考這個

用 linked-list 實作 queue

struct linked_list {
    struct linked_list *next;
    uint8_t data;
};

struct Queue {
    struct linked_list *head, *tail;
    void (*qpush)(uint8_t);
    void (*qpop)();
};

struct Queue queue = NULL;

void qpush(uint8_t val)
{
    struct linked_list *tmp = malloc(sizeof(struct linked_list));
    tmp->next = NULL;
    tmp->val = val;
    if (queue->head == NULL)
        queue->head = tmp;
    else
        queue->tail->next = tmp;

    queue->tail = tmp;
}

void qpop()
{
    struct ListNode *tmp = malloc(sizeof(struct ListNode));
    queue->tail->next = NULL;
    queue->head = queue->head->next;
    
    if (queue->head == NULL)
        queue->tail = NULL;

    free(tmp);
    tmp = NULL;
}

void init_queue(struct Queue **q)
{
    *q = (struct Queue *)malloc(sizeof(struct Queue));
    if (*q == NULL) return;
    (*q)->head = NULL;
    (*q)->tail = NULL;
    (*q)->qpush = qpush;
    (*q)->qpop = qpop;
}

int main(void)
{
    init_queue(&queue);
    list.push(1);
    list.push(2);
    list.push(3);
    list.pop();
    list.push(4);
    list.pop();
}

用 linked-list 實作 stack

struct linked_list {
    struct linked_list *next;
    uint8_t data;
};

struct Stack {
    struct linked_list *top;
    void (*spush)(uint8_t);
    void (*spop)();
};

struct Stack stack = NULL;

void spush(uint8_t val)
{
    struct linked_list *tmp = malloc(sizeof(struct linked_list));
    tmp->next = NULL;
    tmp->val = val;
    if (stack->head == NULL)
        stack->head = tmp;
    else
        stack->top->next = tmp;

    queue->tail = tmp;
}

void spop()
{
    struct ListNode *tmp = malloc(sizeof(struct ListNode));
    queue->tail->next = NULL;
    queue->head = queue->head->next;
    
    if (queue->head == NULL)
        queue->tail = NULL;

    free(tmp);
    tmp = NULL;
}

void init_stack(struct Stack **s)
{
    *q = (struct Queue *)malloc(sizeof(struct Queue));
    if (*q == NULL) return;
    (*q)->head = NULL;
    (*q)->tail = NULL;
    (*q)->qpush = qpush;
    (*q)->qpop = qpop;
}

int main(void)
{
    init_stack(&stack);
    list.push(1);
    list.push(2);
    list.push(3);
    list.pop();
    list.push(4);
    list.pop();
}

8. 介紹 Mutex

他是用來解決同步問題的一種 Data Type,會有兩個 Atomic operation,分別是 Acquire() 及 Release(),裡面的實作方法是利用一個共享的 boolean 變數控制。

來源:面試準備

9. 介紹 Memory Leak

記憶體流失的意思。主要是記憶體管理失當,像是 C/C++ 如果使用malloc() 取的記憶體空間,就必須在不須使用後利用 free() 適當,如果沒有釋放,則會產生 memory leakage。

來源:面試準備

Behavior Questions

你的優點/缺點

求學過程中,最有印象的一門課

面對衝突,你會怎麼解決?

最有成就感的事?

在團體中是甚麼角色?

在團隊中有無遇到衝突?遇到衝突如何解決?

在團隊中討厭哪種人?為甚麼?

遇到很多件棘手的任務要如何面對?

上一次面對失敗是甚麼時候?你從中學習到甚麼?