跳转至

2022

目的

最近在公司开发一个amrnb/amrwb的转码工具,在网上看了很多资料,主要是这篇AMR在IP域中的编码(rfc3267,4867) 对我帮助很大。 这里补充一下缺少和我自己的理解。

AMR编码介绍

AMR是3GPP组织的一个音频编码规范,目前的手机都使用AMR, 比如公司的测试机华为、小米只支持amrnb/amrwb。amrnb是对窄带信号进行编码(200–3400 Hz),amrwb是对更宽的频率范围进行编码(50–7000 Hz)。 根据香农理论,采样频率要是目标频率的两倍,所以向上取整的amrnb的采样频率是8000Hz, amrwb的采样频率是16000Hz。

bitrate

amrnb和amrwb有多种编码速率(bitrate),并支持在通话过程中通过CMR(codec mode request)协商来变化编码速率,不同于一般的编码器固定一种比特率。除此之外,还有没有人声时的SID(舒适噪音)模式。这时只需要发送一个Mode而不是发送无声的包,从而来降低带宽占用。

amrnb 不同比特率时的语音帧大小

Index Mode Class A bits total speech bits
0 4.75 42 95
1 5.15 49 103
2 5.9 55 118
3 6.7 58 134
4 7.4 61 148
5 7.95 75 159
6 10.2 65 204
7 12.2 81 244

amrwb 不同比特率的语音帧大小

Index AMR-WB codec mode Total number of bits Class A Class B Class C
0 6.60 132 54 78 0
1 8.85 177 64 113 0
2 12.65 253 72 181 0
3 14.25 285 72 213 0
4 15.85 317 72 245 0
5 18.25 365 72 293 0
6 19.85 397 72 325 0
7 23.05 461 72 389 0
8 23.85 477 72 405 0

payload type

amrnb和amrwb使用的动态payload type, 所以在用wireshark分析时,需要经常调整payload-type值

align mode

amrnb和amrwb在SIP协商中有两种align mode,对齐模式和节省带快模式。总结有几个区别 * 对齐模式下CMR占8位,每个TOC占8位,然后每个语音帧补齐为整字节。 * 节省带宽模式下,CMR占4位,每个TOC占6位,然后CMR + TOC + 所有语音帧加起来,最后补齐为整字节。

以单帧的12.2kbps为例 * 语音帧为244位,对齐之后为(244 + 8 - 1)/8 = 31字节。加上1字节的CMR和1字节的TOC, 一共33字节。 * 语音帧为244位,加上4位CMR和6位TOC,一共为254位,对齐后为(254 + 8-1)/8,一种32字节字节。

以两帧的12.2kbps为例 * 两个语音帧分别占用31字节,加上1字节的CMR和2字节的TOC, 一共 65字节。 * 两个语音帧分别占用244位, 加上4位的CMR和6 * 2位的TOC, 对齐后为( 488 + 4 + 12 + 8 - 1)/8,一共63字节。

总之,对齐模式是先对齐成字节再求和,节省带宽模式是先计算位和再对齐成字节。特别是多帧的时候,节省带宽模式算起来更复杂。好在实际情况都是1帧。

基于opencore-amr的编解码

开始用FFmpeg的库来做转码和推流,后来发现不支持节省带宽模式,而且还有个BUG,所以后面自己基于opencore的库写的转码功能。

使用systemd-resolved的mdns

之前的博客中使用了avahi来广播本机的hostname和服务,这对于打印机和其他无屏设备非常重要。而较新版本Linux发行版中systemd-networkd+systemd-resolved同样能实现此功能

Network Manager

目前不少发行版默认的网络管理工具是NetworkManager, 需要先将其关闭和卸载,安装systemd-networkd包

配置systemd-networkd

networkd 中可以配置网桥,tun等设备,也可以直接在其中配置dns和dhcp等,在其中加入MulticastDNS=yes和Multicast=yes来启用mDns

[Match]
Name=eth0

[Network]
Address=192.168.4.42/24
DNS=114.114.114.114
Gataway=192.168.4.1
MulticastDNS=yes

[Link]
Multicast=yes

配置systemd-resolved

systemd-resolved功能类似与dnsmasq, 他会继承systemd-networkd中的dns信息,也会读取hostname用来广播,一般不用修改任何配置,之需要将其stub文件链接到/etc/resov.conf, 这样让系统的dns请求发到systemd-resolved

# ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
# ls -l /etc/resolv.conf
lrwxrwxrwx. 1 root root 37 May 22 22:03 /etc/resolv.conf -> /run/systemd/resolve/stub-resolv.conf

确认效果

通过resolvectl status命令可以查看resolve配置,如下全局的MulticastDNS是启用的,而networkd中的配置是per link的,也处于启用状态,Scopes里显示了mDNS,说明其启用了

      Current Scopes: DNS LLMNR/IPv4 LLMNR/IPv6 mDNS/IPv4 mDNS/IPv6
[root@netpanc ~]# resolvectl status
Global
       LLMNR setting: yes
MulticastDNS setting: yes
  DNSOverTLS setting: no
      DNSSEC setting: allow-downgrade
    DNSSEC supported: yes
          DNSSEC NTA: 10.in-addr.arpa
                      16.172.in-addr.arpa
                      168.192.in-addr.arpa
                      17.172.in-addr.arpa
                      18.172.in-addr.arpa
                      19.172.in-addr.arpa
                      20.172.in-addr.arpa
                      21.172.in-addr.arpa
                      22.172.in-addr.arpa
                      23.172.in-addr.arpa
                      24.172.in-addr.arpa
                      25.172.in-addr.arpa
                      26.172.in-addr.arpa
                      27.172.in-addr.arpa
                      28.172.in-addr.arpa
                      29.172.in-addr.arpa
                      30.172.in-addr.arpa
                      31.172.in-addr.arpa
                      corp
                      d.f.ip6.arpa
                      home
                      internal
                      intranet
                      lan
                      local
                      private
                      test

Link 2 (eth0)
      Current Scopes: DNS LLMNR/IPv4 LLMNR/IPv6 mDNS/IPv4 mDNS/IPv6
       LLMNR setting: yes
MulticastDNS setting: yes
  DNSOverTLS setting: no
      DNSSEC setting: allow-downgrade
    DNSSEC supported: yes
  Current DNS Server: 114.114.114.114
         DNS Servers: 114.114.114.114

在其他设备上ping可以成功

PING netpanc.local (192.168.4.43) 56(84) bytes of data.
64 bytes from netpanc.local (192.168.4.43): icmp_seq=1 ttl=64 time=0.350 ms
64 bytes from netpanc.local (192.168.4.43): icmp_seq=2 ttl=64 time=0.799 ms
64 bytes from netpanc.local (192.168.4.43): icmp_seq=3 ttl=64 time=0.733 ms

参考

https://wiki.archlinux.org/title/Systemd-resolved#mDNS

定义

c/cpp中,序列化是将基本类型或struct变为可存储和传递的形式,比如常用的socket send,将按协议组装的struct(pack)显示转换二进制(序列化)并发送给其他主机。而反序列化是将二进制转成基本类型或struct。另外还有fwrite/fread将结构体保存到文本中。 这种方式高效但容易出错。
而其他语言中可以使用xml, json等高级方式

转换方式

强制类型转换

上面讲到的两个函数send和fwrite,都是将基本类型或struct类型强转为字符串, 这种可能受不同机器的对齐影响

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

size_t fwrite(const void *ptr, size_t size, size_t nmemb,
    FILE *stream);

序列化

struct buffer {
  int a;
  char b;
  double c;
};

int main() {

  struct buffer buf;
  buf.a = 1;
  buf.b = 'a';
  buf.c = 2;

  FILE *f = fopen("/tmp/buf", "w+");
  if (f == NULL) {
    return 1;
  }
  fwrite(&buf, sizeof(struct buffer), 1, f);
}

反序列化

struct buffer {
  int a;
  char b;
  double c;
};

int main() {

  struct buffer buf;

  FILE *f = fopen("/tmp/buf", "r");
  if (f == NULL) {
    return 1;
  }

  fread(&buf, sizeof(struct buffer), 1, f);
  printf("%d--%c--%lf--\n", buf.a, buf.b, buf.c);
}

使用protobuf

syntax = "proto3";
package serialize;

option optimize_for = LITE_RUNTIME;

message Person {
    string name = 1;
    int32 id = 2;
}
tutorial::Person Serialize(std::string name, int id, std::string email){
    tutorial::Person person;
    person.set_name(name);
    person.set_id(id);

    return person;
}

void Deserialize(tutorial::Person person){
    std::cout << person.SerializeAsString();
}

optimize_for option

optimize_for (file option): Can be set to SPEED, CODE_SIZE, or LITE_RUNTIME. This affects the C++ and Java code generators (and possibly third-party generators) in the following ways:

  • SPEED (default): The protocol buffer compiler will generate code for serializing, parsing, and performing other common operations on your message types. This code is highly optimized.
  • CODE_SIZE: The protocol buffer compiler will generate minimal classes and will rely on shared, reflection-based code to implement serialialization, parsing, and various other operations. The generated code will thus be much smaller than with SPEED, but operations will be slower. Classes will still implement exactly the same public API as they do in SPEED mode. This mode is most useful in apps that contain a very large number of .proto files and do not need all of them to be blindingly fast.
  • LITE_RUNTIME: The protocol buffer compiler will generate classes that depend only on the "lite" runtime library (libprotobuf-lite instead of libprotobuf). The lite runtime is much smaller than the full library (around an order of magnitude smaller) but omits certain features like descriptors and reflection. This is particularly useful for apps running on constrained platforms like mobile phones. The compiler will still generate fast implementations of all methods as it does in SPEED mode. Generated classes will only implement the MessageLite interface in each language, which provides only a subset of the methods of the full Message interface.

其他还有如xml, json, ninja

背包问题

有一个承重为V的背包,有N个物品,重量分别为Cost[i], 价值分别为Value[i], 问选择放哪些物品时包的价值最大

状态方程

解决动态编程的关键是找到状态方程,如下。 解决背包问题就是比较不放哪个价值高

F [i, v] ← max{F [i − 1, v], F [i − 1, v − Ci] + Wi}

01背包

要求物品最大为1个, 这里有几个版本

#include <stdio.h>
#include <sys/param.h>

const int V = 4, N = 3;
const int cost[N] = {1, 3, 4};
const int value[N] = {15, 20, 30};

// const int V = 10, N = 5;
// const int cost[N] = {2, 3, 5, 2, 4};
// const int value[N] = {2, 2, 1, 1, 2};

void O1Package_N() {
  int dp[N][V + 1];

  // 0容量的价值为0, 合法
  for (int i = 0; i <= V; i++) {
    dp[0][i] = 0;
  }

  // 初始化当包承重大于物品0重量的情况
  for (int j = cost[0]; j <= V; j++) {
    // 当只装物品0时,包的价值就是物品0的价值
    dp[0][j] = value[0];
  }

  // 先遍历物品,后遍历重量
  for (int i = 1; i < N; i++) {
    for (int v = 0; v <= V; v++) {
      // F [i, v] ← max{F [i − 1, v], F [i − 1, v − Ci] + Wi}
      // F[i][v] = MAX(F[i - 1][v], F[i - 1][v - cost[i]] + value[i]);
      int bufang = dp[i - 1][v];
      dp[i][v] = bufang;
      if (v >= cost[i]) {
        int fang = dp[i - 1][v - cost[i]] + value[i];
        dp[i][v] = MAX(bufang, fang);
      }
    }
  }

  for (int i = 0; i < N; i++) {
    for (int j = 0; j <= V; j++) {
      printf("%d\t", dp[i][j]);
    }
    printf("\n");
  }
  printf("%d\n", dp[N - 1][V]);
}

void O1Package_N_plus_1() {

  int dp[N + 1][V + 1];

  // 使用0个物品的价值为0
  for (int i = 0; i <= V; i++) {
    dp[0][i] = 0;
  }

  // 先遍历物品,后遍历重量
  for (int i = 1; i <= N; i++) {
    for (int v = 0; v <= V; v++) {
      dp[i][v] = dp[i - 1][v]; //当重量不够装i时,价值为装i-1的价值,
                               //此步骤以备i+1时使用(下一个i循环)
      if (v >= cost[i - 1]) {
        dp[i][v] = MAX(dp[i - 1][v], dp[i - 1][v - cost[i - 1]] + value[i - 1]);
      }
    }
  }

  for (int i = 0; i <= N; i++) {
    for (int j = 0; j <= V; j++) {
      printf("%d\t", dp[i][j]);
    }
    printf("\n");
  }
  printf("%d\n", dp[N][V]);
}

void O1Package_N_plus_1_origin() {

  int dp[N + 1][V + 1];

  // 使用0个物品的价值为0
  for (int i = 0; i <= V; i++) {
    dp[0][i] = 0;
  }

  // 先遍历物品,后遍历重量
  for (int i = 0; i < N; i++) {
    for (int v = 0; v <= V; v++) {
      // 1. 当物品大于承重,物品不会放到背包,价值等于不放此物品时的情况
      // 即使物品小于承重,说明物品会放到背包。提前多赋值一次也是允许的
      dp[i + 1][v] = dp[i][v];
      if (v >= cost[i]) {
        dp[i + 1][v] = MAX(dp[i][v], dp[i][v - cost[i]] + value[i]);
      }
    }
  }

  for (int i = 0; i <= N; i++) {
    for (int j = 0; j <= V; j++) {
      printf("%d\t", dp[i][j]);
    }
    printf("\n");
  }
  printf("%d\n", dp[N][V]);
}

void O1Package_one_array_n() {

  int dp[V + 1];

  // 使用0个物品的价值为0
  for (int i = 0; i <= V; i++) {
    dp[i] = 0;
  }

  // 先遍历物品,后遍历重量
  for (int i = N - 1; i >= 0; i--) {
    for (int v = V; v >= cost[i]; v--) {
      int bufang = dp[v];
      int fang = dp[v - cost[i]] + value[i];
      dp[v] = MAX(bufang, fang);
    }
  }

  for (int j = 0; j <= V; j++) {
    printf("%d\t", dp[j]);
  }
  printf("\n");
  printf("%d\n", dp[V]);
}

完全背包

要求物品可以取无限个,但由于包的承重为V,所以每种物品最大数量为 V/cost[i]

void CompletePackage_use_n() {

  int dp[N][V + 1];

  // 0容量的价值为0, 合法
  for (int i = 0; i < cost[0]; i++) {
    dp[0][i] = 0;
  }

  // 初始化当包承重大于物品0重量的情况
  for (int i = cost[0]; i <= V; i++) {
    // 当只装物品0时,包的价值就是物品0的价值
    dp[0][i] = i / cost[0] * value[0];
  }

  // 先遍历物品,后遍历重量
  for (int i = 1; i < N; i++) {
    for (int v = 0; v <= V; v++) {
      dp[i][v] = dp[i - 1][v]; //当重量不够装i时,价值为装i-1的价值,
                               //此步骤以备i+1时使用(下一个i循环)
      for (int k = 1; k <= v / cost[i]; k++) {
        dp[i][v] = MAX(dp[i - 1][v], dp[i - 1][v - k * cost[i]] + k * value[i]);
      }
    }
  }

  for (int i = 0; i < N; i++) {
    for (int j = 0; j <= V; j++) {
      printf("%d\t", dp[i][j]);
    }
    printf("\n");
  }
  printf("%d\n", dp[N - 1][V]);
}

void CompletePackage_use_n_plus_1() {

  int dp[N + 1][V + 1];

  // 0容量的价值为0, 合法
  for (int i = 0; i <= V; i++) {
    dp[0][i] = 0;
  }

  // 先遍历物品,后遍历重量
  for (int i = 1; i <= N; i++) {
    for (int v = 0; v <= V; v++) {
      dp[i][v] = dp[i - 1][v]; //当重量不够装i时,价值为装i-1的价值,
                               //此步骤以备i+1时使用(下一个i循环)
      for (int k = 1; k <= v / cost[i-1]; k++) {
        dp[i][v] = MAX(dp[i - 1][v], dp[i - 1][v - k * cost[i-1]] + k * value[i-1]);
      }
    }
  }

  for (int i = 0; i <= N; i++) {
    for (int j = 0; j <= V; j++) {
      printf("%d\t", dp[i][j]);
    }
    printf("\n");
  }
  printf("%d\n", dp[N][V]);
}

void CompletePackage_use_one_array_no_reverse_V() {

  int dp[V + 1];

  // 0容量的价值为0, 合法
  for (int i = 0; i <= V; i++) {
    dp[i] = 0;
  }

  // 先遍历物品,后遍历重量
  for (int i = 1; i <= N; i++) {
    for (int v = 0; v <= V; v++) {
      for (int k = 1; k <= v / cost[i-1]; k++) {
        dp[v] = MAX(dp[v], dp[v - k * cost[i-1]] + k * value[i-1]);
      }
    }
  }

  for (int j = 0; j <= V; j++) {
    printf("%d\t", dp[j]);
  }
  printf("\n");
  printf("%d\n", dp[V]);
}

void CompletePackage_use_one_array_reverse_V() {

  int dp[V + 1];

  // 0容量的价值为0, 合法
  for (int i = 0; i <= V; i++) {
    dp[i] = 0;
  }

  // 先遍历物品,后遍历重量
  for (int i = 1; i <= N; i++) {
    for (int v = V; v <= cost[i-1]; v--) {
      for (int k = 1; k <= v / cost[i-1]; k++) {
        dp[v] = MAX(dp[v], dp[v - k * cost[i-1]] + k * value[i-1]);
      }
    }
  }

  for (int j = 0; j <= V; j++) {
    printf("%d\t", dp[j]);
  }
  printf("\n");
  printf("%d\n", dp[V]);
}

多重背包

多重背包规定了每个物品的最大数量cap[i], 但由于要考虑背包承重,所以真正可以容纳物品的最大个数是max(cap[i], V/cost[i])

疑问

为何不需要将背包排序, 比如按重量排序或按价值排序,再将重量下而价值高的物品优先计算?

即使没有排序,由于第二层循环都是从0->V遍历和放物品i与不放物品i比较,所以顺序不重要

  for (int i = 1; i < N; i++) {
    for (int v = 0; v <= V; v++) {
      int bufang = dp[i - 1][v];
      dp[i][v] = bufang;
      if (v >= cost[i]) {
        int fang = dp[i - 1][v - cost[i]] + value[i];
        dp[i][v] = MAX(bufang, fang);
      }
    }
  }

算法复杂度和优化

矩阵数组的时间复杂度为O(VN),即两层for循环;空间复杂度为O(VN) 即申请一个矩形数组,经过一元数组优化后,空间复杂度变为O(V) 除此之外, 大多情况下,通过两层遍历,可以只保留重量相同且价值最大的那个物品,这个操作的时间复杂度为O(N^2), 但当最糟糕的情况,即精心设计的物品没有相同重量或者相同价值的情况则此优化无效 另外,先加物品重量大于背包重量的物品忽略,可以加快速度

理解优化

无论是用矩阵数组还是优化成一元数组,最好的理解方式是单步调试一遍

N行与N+1行

两种方式可以通用,只是当N行时,需要提前初始化物品0的情况

初始化问题

我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。一种区别这两种问法的实现方法是在初始化的时候有所不同。

如果是第一种问法,要求恰好装满背包,那么在初始化时除了 F[0] 为 0,其它F[1..V ] 均设为 −∞,这样就可以保证最终得到的 F[V ] 是一种恰好装满背包的最优解。 如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将 F[0..V ]全部设为 0。

这是为什么呢?可以这样理解:初始化的 F 数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为 0 的背包可以在什么也不装且价值为 0 的情况下被“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,应该被赋值为 -∞ 了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为 0,所以初始时状态的值也就全部为 0了。

贪心算法

由于背包问题里的物品具备重量价值两个维度,所以无法用贪心算法解决。而当维度为1的时候,可以用贪心算法,例如~零钱问题~和会议安排问题,因为不同零钱价值不同但其“重量”都为1,会议的“重量”也为1 : leetcode上的零钱问题不能用贪心算法

参考

《背包问题九讲》

FakeIt

FakeIt是一个单元测试工具,类比gmock, 但是比gmock更modern和轻量,因为它支持c++11和header-only

使用方法

FakeIt和gmock都提供Mock和调用计数功能,Mock通常用于接口类,用来定义并实例化接口类对象。gmock和FakeIt都能对调用参数进行校验,但FakeIt的Verify函数还提供在校验失败时打印参数值,这是gmock不具备的

struct IAppManager {
  // inotify calls when new event
  virtual void notify_add_event(EventType event_type,
                                const std::string& filepath) = 0;
  virtual void notify_rename_event(EventType event_type,
                                   const std::string& filepath,
                                   const std::string& filepath_new) = 0;

  virtual ~IAppManager() = default;
};

TEST_CASE("Test Inotify") {
  Mock<IAppManager> appmanager_mock;
  Fake(Dtor(appmanager_mock));
  Fake(Method(appmanager_mock, notify_add_event));
  Fake(Method(appmanager_mock, notify_rename_event));

  auto& app_manager = appmanager_mock.get();
  SECTION("test add") {
    std::thread t1([&app_manager](){
      app_manager.notify_add_event(EventType::File, "filename");
      app_manager.notify_add_event(EventType::File, "filename");
      app_manager.notify_add_event(EventType::File, "filename");
    });
    t1.join();
    Verify(Method(appmanager_mock, notify_add_event)).Exactly(2);
  }
}

这里没有对调用参数校验,而仅对调用次数验证,可以预见,结果失败,因为调用了3次。

问题

上面的测试用例会失败,因为次数不匹配。而这时FakeIt会打印所有调用的参数值,但参数由于是个引用,而且引用在线程退出时被释放,打印一个无效引用导致会抛异常

due to a fatal error condition:                                                                 
  SIGSEGV - Segmentation violation signal 

解决办法

对于本测试而言,解决办法就是将传参所引用的对象duration改大,如全局变量或者static,或者在线程外定义它

TEST_CASE("Test Inotify") {
  Mock<IAppManager> appmanager_mock;
  Fake(Dtor(appmanager_mock));
  Fake(Method(appmanager_mock, notify_add_event));
  Fake(Method(appmanager_mock, notify_rename_event));

  auto& app_manager = appmanager_mock.get();
  SECTION("test add") {
    std::string filename = "asd";
    std::thread t1([&app_manager, &filename](){
      app_manager.notify_add_event(EventType::File, filename);
      app_manager.notify_add_event(EventType::File, filename);
      app_manager.notify_add_event(EventType::File, filename);
    });
    t1.join();
    Verify(Method(appmanager_mock, notify_add_event)).Exactly(2);
  }
}
修改之后,不会触发SIGSEGV异常, 而是能正确打印出调用参数

  Expected pattern: appmanager_mock.notify_add_event( Any arguments )
  Expected matches: exactly 2
  Actual matches  : 3
  Actual sequence : total of 3 actual invocations:                             
    appmanager_mock.notify_add_event(?, asd)
    appmanager_mock.notify_add_event(?, asd)
    appmanager_mock.notify_add_event(?, asd) 

但对于间接调用的接口,且在单元测试里无法控制变量类型,则没有什么办法,只能保证Extractly准确而不用打印参数值

gmock的方式

gmock使用预先设定调用参数,在调用时再判断,所以不会出现无效引用的问题

print(stringify) variable of different types

typeof is a GCC extension, and _Generic is part of C11. typeof is disabled by option '-std=*' or '-ansi, so usetypeof` instead.

c11 _Generic

#include <stdio.h>

#define PRINT_VAR(var)        \
    _Generic((var),           \
        int: print_int,       \
        float: print_float,   \
        double: print_double, \
        char: print_char      \
    )(#var, (var))

// Type-specific print functions
void print_int(const char* name, int value) {
    printf("%s = %d\n", name, value);
}

void print_float(const char* name, float value) {
    printf("%s = %.2f\n", name, value);
}

void print_double(const char* name, double value) {
    printf("%s = %.2f\n", name, value);
}

void print_char(const char* name, char value) {
    printf("%s = %c\n", name, value);
}

int main() {
    int x = 10;
    float y = 3.14f;
    double z = 3.14159;
    char c = 'A';

    PRINT_VAR(x);  // Prints: x = 10
    PRINT_VAR(y);  // Prints: y = 3.14
    PRINT_VAR(z);  // Prints: z = 3.14
    PRINT_VAR(c);  // Prints: c = A

    return 0;
}

simpler way

linter raise warnings if place printf directly in _Generic.

#define DECLARE_PRINT_FUNC(value)                                              \
  do {                                                                         \
    printf("%s = ", #value);                                                   \
    char *format = NULL;                                                       \
    _Generic((value),                                                          \
        int: format = "%d\n",                                                  \
        long unsigned int: format = "%lu\n",                                   \
        long: format = "%ld\n",                                                \
        float: format = "%.2f\n",                                              \
        double: format = "%.2f\n",                                             \
        char: format = "%c\n",                                                 \
        default: format = NULL);                                               \
    format != NULL ? printf(format, (value)):                                  \
    printf("Unknown type\n");                                                  \
  } while (0)

using GCC build-in typeof instead of _Generic

#include <stdio.h>

#define PRINT_VAR_TYPEOF(var)                                                  \
  do {                                                                         \
    printf("%s = ", #var);                                                     \
    char *format = NULL;                                                       \
    __typeof__(var) _v = (var);                                                \
    if (__builtin_types_compatible_p(__typeof__(_v), int))                     \
      format = "%d\n";                                                         \
    else if (__builtin_types_compatible_p(__typeof__(_v), float))              \
      format = "%.2f\n";                                                       \
    else if (__builtin_types_compatible_p(__typeof__(_v), double))             \
      format = "%.2f\n";                                                       \
    else if (__builtin_types_compatible_p(__typeof__(_v), char))               \
      format = "%c\n";                                                         \
    printf(format, (var));                                                     \
  } while (0)

三指针法翻转单向链表

定义3个指针, 原理是三个指针分别指向前三个元素,将第一个元素指向第二个元素,然后全部后移 1. 第三个指针向前移动 2. 将第二个元素指向第一个元素 3. 第一个指针和第二个指针向前移动

struct Node{
  int data;
  Node *next;
};

Node *reverse(Node *head) {
  Node *pPrev = nullptr;
  Node *pHead = head;
  Node *pNext = nullptr;
  while (pHead) {
    pNext = pHead->next;
    pHead->next = pPrev;
    pPrev = pHead;
    pHead = pNext;
  }
  return pHead;
}
Node *reverse(Node *head) {
  if (!head) {
    return head;
  }
  Node *first = nullptr;
  Node *second = head;
  Node *third = head->next;

  while (third) {
    second->next = first;

    first = second;
    second = third;
    third = third->next;
  }

  second->next = first;

  return second;
}

翻转链表的部分节点

翻转node[m]->node[N]部分, 理解时先将链表分为3部分: part1->part2->part3, 只翻转part2

翻转动作套用三指针法,但部分翻转的难点在于处理part1为空和非空的情况,所以要判断m时候为1 * m==1即要翻转第一个元素时,需要先记录part2翻转后的tail node,最后将其next指向part3的第一个元素 * m!=1即part1不为空,需要先记录part2翻转后的tail node和part1的最后一个node,最后将其指向part2的新head,以及将part2的第一个node指向part3

struct ListNode *reverseBetween(struct ListNode *head, int m, int n) {
  if (m == n) {
    return head;
  }

  if(!head || !head->next){
    return head;
  }

  // write code here
  struct ListNode *pleft = NULL;
  struct ListNode *phead = head;
  struct ListNode *pright = NULL;


  // 只有当m!=1时才需要,指向part1的最后一个元素
  struct ListNode *pPart1Tail = NULL;
  // 当m=1时指向part2的第一个元素
  struct ListNode *pPart2NewTail = NULL;

  // skip m
  for (int i = 1; i < m; i++) {
    pleft = phead; 
    phead = phead->next;
  }

  // 保存part2的head,也是翻转后的tail
  pPart2NewTail = phead;
  // m != 1 时pleft不为NULL
  struct ListNode *pPartOneTail = pleft;

  int i = 0;
  // 翻转次数要比m-n大1
  while (phead && i < n - m + 1) {
    pright = phead->next;

    phead->next = pleft;

    pleft = phead;
    phead = pright;
    i++;
  }

  if(m==1){
    // 指向part3的头
    pPart2NewTail->next = phead;
    return pleft;
  }

  pPartOneTail->next = pleft;
  pPart2NewTail->next = phead;

  return head;
}

在Linux下使用sdcc进行单片机开发

周末研究了下单片机,本来准备写个定时器来控制电扇开关,发现比起树梅派,单片机实在不太方便

开发环境搭建

Windows大家都用Keil, 一站式比较方便,但这是个收费软件。而在Linux下,使用到一些组合来实现编译和烧录

  1. sdcc 开源的编译器,类似gcc
  2. stcflash github上的开发者开发的烧录脚本

sdcc 在Debian有包可以直接安装

# apt install sdcc
再 clone github的代码到本地,需要使用root权限执行烧录。而我执行是发现缺少python serial包,要全局安装就使用apt命令

# apt install python3-serial

这样环境就搭建好了

编写代码

用c语言开发时要引入比较的定义头文件,我的单片机是好多年前买的国产stc 89C52RC, 是基于51的拓展版本,于是用了sdcc的8052.h头,路径在

// /usr/share/sdcc/include/mcs51/8052.h
#ifndef REG8052_H
#define REG8052_H

#include <8051.h>     /* load definitions for the 8051 core */

#ifdef REG8051_H
#undef REG8051_H
#endif

不过不需要我们指定路径,sdcc会自动引入。窥视其内容可以发现它包含了8051的东西,所以说52是基于51的

回顾一下51单片机知识

  1. 单片机的寄存器初始是0, 而引脚初始都是高点位
  2. 定时器和计数器的触发事件都是靠中断,中断执行当前(main)中的函数而执行中断响应函数,这跟Unix内核是一样的。 单片机还具有多个优先级的中断,可以嵌套
  3. 中断的触发靠计数寄存器(TLx和THx)溢出,这两个都是8位寄存器,所以同时使用时,在2^16时溢出。但也有只用其中一个寄存器的时候,这由TMOD寄存器控制

TMOD寄存器

TMOD是不可按位访问的寄存器,意思是只能一次性赋值所有位。 TMOD控制着两个定时器/计数器0和1

timer.png

  • TMOD高四位用于设置定时器/计数器1,低四位用于设置定时器/计数器0;
  • GATE寄存器决定是否使用外部INTx控制,即软件自动启动,还是外部启动,这里赋值0
  • GATE=1时,“与门”的输出信号K由INTx输入电平和TRx位的状态一起决定(即此时K=TRx·INTx),当且仅当TRx=1,INTx=1(高电平)时,计数启动;否则,计数停止。当INT0引脚为高平时且TR0置位,TR0=1;启动定时器T0
  • GATE=0时,“A”输出恒为1,“B”的值由TRx决定,当TR0=1,启动定时器/计数器T0,当TR1=1,启动定时器/计数器T1
  • CT位为1时选择计数器模式,为0时选择定时器模式
  • M1,M0用于选择工作方式,当M0=1,M1=0时,使用TL0和TH0的16位来计数

TCON寄存器

TCON.png

定时器初始化

这里使用定时器0

void Timer0Init(void) // 1毫秒@12.000MHz
{
    TMOD = 0x01; //设置定时器模式 0000 0001
    // TCON
    TF0 = 0; //初始化溢出标志
    TR0 = 1; //定时器0开始计时
    // 计数器初始值,再触发一次就溢出了
    TH0 = 0xFF; //设置定时初值
    TL0 = 0xFE; //设置定时初值
    // 中断通路
    EA = 1; 
    ET0 = 1;
    PT0 = 0;
}

计数器模式

这里使用计数器0

void Count0Init(void) // 1毫秒@12.000MHz
{ 
    TMOD = 0x05; //设置计数器模式 0000 0101

    // TCON
    TF0 = 0; 
    TR0 = 1; //定时器0开始计时

    // 计数器初始值,再触发一次就溢出了
    TH0 = 0xFF;  //设置定时初值
    TL0 = 0xFE;  //设置定时初值

    // 中断通路
    EA = 1;
    ET0 = 1;
    PT0 = 0;
}

中断响应函数

sdcc和keil有一些区别,中断标志符使用__interrupt

static unsigned int T0Count = 0;
void timer0_routine() __interrupt (1) {
  T0Count++;
  TH0 = 0xFF; //设置定时初值
  TL0 = 0xFE; //设置定时初值
  if (T0Count == 3) {
    T0Count = 0;
    P1_1 = 0;  // 0输出低电平,灯泡亮
  }
}

完整代码

// main.c
#include "8052.h"

void Timer0Init(void) // 1毫秒@12.000MHz
{
  TMOD = 0x01;
  TF0 = 0;
  TR0 = 1;
  TH0 = 0xFF;
  TL0 = 0xFE;
  EA = 1;
  ET0 = 1;
  PT0 = 0;
}

void main(void) {

  Timer1Init();
  while (1)
    ;
}

unsigned int T0Count = 0;
void timer0_routine() __interrupt(1) {
  T0Count++;
  TH0 = 0xFF;
  TL0 = 0xFE;
  if (T0Count == 3) {
    T0Count = 0;
    P1_1 = 0;
  }
}

编译和烧录

代码保存为main.c

# 使用sdcc编译
sdcc main.c
将sdcc生成的ihx文件转为hex文件
packihx main.ihx  > main.hex   

烧录到单片机

sudo python ~/github/stcflash/stcflash.py main.hex
Connect to /dev/ttyUSB0 at baudrate 2400
Detecting target... done
 FOSC: 11.955MHz
 Model: STC89C52RC (ver4.3C) 
 ROM: 8KB
 [X] Reset stops watchdog
 [X] Internal XRAM
 [X] Normal ALE pin
 [X] Full gain oscillator
 [X] Not erase data EEPROM
 [X] Download regardless of P1
 [X] 12T mode
Baudrate: 38400
Erasing target... done
Size of the binary: 147
Programming: #################### done
Setting options... done

数据库优化总结

这里总结了从数据库设计和sql结构两个方面的优化

数据库设计范式 Normal Form

范式是在数据库的设计时总结出来的一套规范,通常满足3范式就足够了。但由于3范式也存在问题,所以有了3.5范式

范式从1到3是越来越严格,所以满足1NF不一定满足2NF,但满足2NF就肯定满足1NF

满足所有范式的表没有冗余数据,但也有问题,比如效率不高。这就有了反范式做法,即允许数据冗余,用空间换时间

1NF:原子化,单元格里的值不能被分割

Employee Id Employee Name Phone Number Salary
1EDU001 Alex +91 85532065
+91 85532066
60,131
1EDU002 Alex +91 85532066 75,331
1EDU003 Alex +91 85532068 63,231
1EDU004 Alex +91 85532069 70,231
1EDU005 Alex +91 85543065
+91 85543075
60,531

可见,有条目的电话格里有两个号码,是可以分割的, 所以不满足1NF。应改为

Employee Id Employee Name Phone Number Salary
1EDU001 Alex +91 85532065 60,131
1EDU001 Alex +91 85532066 60,131
1EDU002 Alex +91 85532066 75,331
1EDU003 Alex +91 85532068 63,231
1EDU004 Alex +91 85532069 70,231
1EDU005 Alex +91 85543065 60,531
1EDU005 Alex +91 85543075 60,531

当所有单元格都无法分割了,就满足了第一范式

2NF:属性不允许部分依赖(复合)主键

满足2NF前提要满足1NF, 范式2要求非主属性必须依赖完整复合主键或者主键。

下表Employee IdDepartment Id组成复合主键
|Employee Id|Department Id |Office Location | |---|---|---| |1EDUO01 | ED-T1 | Pune | |1EDUO02 | ED-S2 |Bengaluru | |1EDUO03 | ED-M1 | Delhi | |1EDUO04 | ED-T3 | Mumbai | |1EDUO05 | ED-T1 | Pune |

Office Location只依赖主键的Employee Id部分。或者说只需Department Id就能保证Office Location唯一, 所以是部分依赖。这样不满足2NF了,应改成两个表

Employee Id Department Id
1EDUO01 ED-T1
1EDUO02 ED-S2
1EDUO03 ED-M1
1EDUO04 ED-T3
1EDUO05 ED-T1
Department Id Office Location
ED-T1 Pune
ED-S2 Bengaluru
ED-M1 Delhi
ED-T3 Mumbai

3NF: 依赖不能传递

满足3NF前提要满足2NF, 还有非主属性必须依赖主键/复合主键,不能依赖其他非主属性

Student Id Student name Subject Id Subject Adress
1DT1SENGO1 Alex 15CS11 SQL Goa
1DT1SENGO2 Barry 15CS13 JAVA Bengaluru
1DT15ENGO3 Clair 15CS12 C++ Delhi
1DT15ENG04 DavId 15CS13 JAVA Kochi

上表的Subject依赖非主属性Subject Id,而不是依赖Student Id, 这就不满足3NF了。应改成

Student Id Student name Subject Id
1DT1SENGO1 Alex 15CS11
1DT1SENGO2 Barry 15CS13
1DT15ENGO3 Clair 15CS12
1DT15ENG04 DavId 15CS13
Subject Id Subject Adress
15CS11 SQL Goa
15CS13 JAVA Bengaluru
15CS12 C++ Delhi
15CS13 JAVA Kochi
不满足3范式的问题

不满足3范式的数据库会存在以下问题: * 插入问题: 插入数据时,如果只有部分数据,可能导致插入失败(若某列为NOT NULL)。举例2NF里原来的表,若只有Department IdOffice Location,就无法插入 * 更新问题: 当更新某个字段时,需要更新其它列。举例2NF的表,当更新Pune时, 第1列和第4列都要更新 * 删除问题: 当删除某些属性时,其他属性也要删除, 举例3NF原来的表,当删除SQL课程时,学生Alex的信息也会丢失

3.5NF: BCNF

3.5范式用于解决3范式遗留问题
例如

Student Id Subject Professor
1DT15ENG01 SQL Prof. Mishra
1DT15ENG02 JAVA Prof. Anand
1DT15ENG02 C++ Prof. Kanthi
1DT15ENG03 JAVA Prof. Anand
1DT15ENG04 DBMS Prof. Lokesh

说明: * 一个学生可以登记多个科目 * 多个老师可以教授同一个科目 * 每个科目会有一个老师分配给一个学生

所以这个表满足3范式,因为Student IdSubject组成了复合主键,决定了Professor

但是不满足3.5NF,因为Student IdSubject组成了复合主键,所以Subject是主属性, 而存在Professor依赖Subject, 虽然Subject是主属性,但Professor是非主属性,所以不满足BCNF。

此表应改成

Student Id Professor Id
1DT15ENG01 1DTPF01
1DT15ENG02 1DTPF02
1DT15ENG02 1DTPF03
Professor ID Professor Subject
1DTPF01 Prof. Mishra SQL
1DTPF02 Prof. Anand JAVA
1DTPF03 Prof. Kanthi C++

反范式

没有冗余数据的设计未必是好设计,有时为了效率,必须降低范式标准,适当允许保留冗余数据,以达到用空间换时间的目的。

例如订单表存在商品单价数量总价, 这里总价是冗余的,因为可以通过单价和数量来求得,但总价经常需要,所以冗余可以提高效率。

HAVING 和查询语句优先级

SQL查询字句类似C语言的运算符优先级, 虽然标准里没有定义优先级,但大致为以下

FROM > WHERE > SELECT > GROUP BY > HAVING > ORDER BY
这个顺序可以帮忙理解查询语句是如何执行的,比如因为SELECT优先级低于FROM, 所有只能在FROM中定义alais在SELECT中使用,而不能在SELECT中定义alias并在FROM中使用

HAVING指令通常和GROUP一起用,因为优先级比GROUP低,所以HAVING可以计算出GROUP的值并进行筛选。

SELECT Sno FROM Grade GROUP BY Sno HAVING COUNT(Cno) > 3;
这个语句筛选出相同Sno记录的个数>3的集合

VIEW和数据冗余

View是一种虚拟表,可以将多表的数据关联在一起,也可以做一些冗余数据,如计算平均值

CREATE VIEW Cl_avg_age AS SELECT Clno, AVG(Sage) AS avg_age FROM Student;

事物,存储过程和触发器

存储过程需要提前创建再使用。 因为存储过程中定义变量, 输入输出明确,所以让数据库操作更安全。

CREATE PROCEDURE ap_returncount @Sno char(7) AS DECLARE @count int SELECT @count = Number FROM Class WHERE Clno = (SELECT Clno FROM Student WHERE Sno = @Sno) RETURN @count;

触发器是当触发条件时自动执行的命令,如当插入和更新满足条件时自动执行事物回撤。这是基于事物机制的。 事物可以保证多个操作的原子性, 当莫个操作不满足条件则全部ROLLBACK,回撤所有的操作,例如典型的转账场景,必须要扣除和增加都成功时才能成功。

但事物导致效率比较低。

CREATE TRIGGER tri_ins_upd_student ON Student AFTER INSERT, UPDATE AS IF 40 < (SELECT Number FROM Class WHERE Clno = (SELECT Clno FROM inserted )) ROLLBACK TRANSACTION;

参考

https://www.edureka.co/blog/normalization-in-sql/