Этот текст адресован когорте программистов на С(ях). Это не академические атрибуты из учебников это скорее правила буравчика оформления сорцов из реального prod(а). Некоторые приемы совпали с MISRA, некоторые с CERT-C. А кое-что является результатом множества итераций инспекций программ и перестроек после реальных инцидентов. В общем тут представлен обогащенный концентрат полезных практик программирования на С(ях).
*1–Все функции должны быть менее 45 строк. Так каждая функция сможет уместиться на одном экране. Это позволит легко анализировать алгоритм и управлять модульностью кода. Также множество мелких функций удобнее покрывать модульными тестами.
*2–Не допускать всяческих магических чисел в коде. Это уничтожает читаемость кода. Все константы надо определять в перечисления заглавными буквами в отдельном файле для каждого программного компонента.
*3–На все сборки должна быть одна общая кодовая база (общак, репа). Модификация в одном компоненте должна отражаться на всех сборках организации, использующих компонент (например алгоритмы CRC). Это позволит сэкономить время на создание новых проектов для новых программ.
*4–Все .с файлы должны быть оснащены одноименным .h файлом. Так эффективнее переносить, анализировать и мигрировать проекты на очередные аппаратные платформы. И сразу понятно, где следует искать прототипы функций из *.c файлов.
*5–Аппаратно-зависимый код должен быть отделен от аппаратно независимого кода по разным файлам и разным папкам. Так можно тестировать на другой архитектуре платформо-независимые функции и алгоритмы. Всякую математику, калькуляторы всяческих CRC(шек) и работу со строчками.
6--Константы следует определять при помощи перечислений enum в большей степени, чем препроцессором. Так можно собрать константы из одной темы в одном месте и они не будут разбросаны по всему проекту.
7–Не вставлять функции внутрь if() . Коды возврата приходится анализировать пошаговым отладчиком до проверки условия.
это очень плохо:
Надо писать код так, чтобы было возможно его проверять пошаговым отладчиком. Поэтому каждое элементарное действие должно быть на одной строке. Вот так уже гораздо лучше.
8–Использовать static функции везде, где только можно. Это повысит модульность.
*9–Используй препроцессорный #error для предупреждения о нарушении зависимостей между компонентами.
*10--Если что-то можно проверить на этапе make файлов, то это надо проверить на этапе make файлов. Каждый компонент должен проверять, что подключены нужные зависимости. Это можно сделать через условные операторы make файлов.
*11--Если что-то можно проверить на этапе препроцессора, то это надо проверить на этапе препроцессора. Каждый компонент должен проверять, что подключены нужные зависимости. Это можно сделать через макросы компонентов.
*12–Если что-то можно проверить на этапе компиляции, то это надо проверить на этапе компиляции (static_assert(ы)). Например можно проверить, что в конфигурациях скорость UART не равна нулю. В RunTime не должно быть проверок, которые можно произвести на этапе компиляции, препроцессора или make файлов.
*13–Каждой set функции должна быть поставлена в соответствие get функция. И наоборот. Это позволит написать модульный тест для данного параметра.
*14–Если переменная это физическая величина, то в суффиксе указывать размерность (timeout_ms). Это увеличивает понятность кода.
*15–Все Си-функции должны всегда возвращать код ошибки. Минимум тип bool или числовой код ошибки. Так можно понять, где именно что-то пошло не так. Проще говоря, не должно быть функций, которые возвращают void. Функции void это, по факту, бомбы с часовым механизмом. В один день они отработают ошибочно, а вы об этом ничего даже не узнаете.
*16–Для каждого программного компонента создавать несколько *.с *.h файлов:
Это позволит ориентироваться в коде и управлять модульностью.
17–Если функция получает указатель, то пусть сразу проверяет на нуль значение указателя. Так прошивки не будут падать при получении нулевых указателей. Это повысит надежность кода. Вы же не знаете как и кто этот код будет испытывать. Хорошая функция всегда проверяет то, что ей дают.
18–Если есть конечный автомат, то добавить счетчик циклов. Так можно будет проверить, что автомат вообще вертится.
19–В идеале все переменные должны иметь разные имена. Так было бы очень удобно делать поиск по grep. Но тут надо искать компромисс с наглядностью.
20–У каждой функции должен быть только 1 return. Это позволит дописать какой-то функционал в конце, зная, что он точно вызовется.
21–Не использовать операторы >, >= Вместо них использовать <, <= просто поменяв местами аргументы там, где это нужно. Это позволит интуитивно проще анализировать логику по коду. Человеку еще со времен школьной математики понятнее, когда то, что слева - то меньше, а то, что справа - то больше. Так как ось X стрелкой показывала вправо. Особенно удобно при проверке переменной на принадлежность интервалу. Получается, что > и >= это вообще два бессмысленных оператора в языке С.
*22–В проекте обязательно должны быть модульные тесты. Тесты это просто функции, которые вызывают другие функции в run-time. Это позволит сделать безболезненную перестройку кода, когда архитектура начнет скрипеть. Тесты можно вызывать как до запуска приложения, так и по команде из UART- CLI.
*1–Все функции должны быть менее 45 строк. Так каждая функция сможет уместиться на одном экране. Это позволит легко анализировать алгоритм и управлять модульностью кода. Также множество мелких функций удобнее покрывать модульными тестами.
*2–Не допускать всяческих магических чисел в коде. Это уничтожает читаемость кода. Все константы надо определять в перечисления заглавными буквами в отдельном файле для каждого программного компонента.
*3–На все сборки должна быть одна общая кодовая база (общак, репа). Модификация в одном компоненте должна отражаться на всех сборках организации, использующих компонент (например алгоритмы CRC). Это позволит сэкономить время на создание новых проектов для новых программ.
*4–Все .с файлы должны быть оснащены одноименным .h файлом. Так эффективнее переносить, анализировать и мигрировать проекты на очередные аппаратные платформы. И сразу понятно, где следует искать прототипы функций из *.c файлов.
*5–Аппаратно-зависимый код должен быть отделен от аппаратно независимого кода по разным файлам и разным папкам. Так можно тестировать на другой архитектуре платформо-независимые функции и алгоритмы. Всякую математику, калькуляторы всяческих CRC(шек) и работу со строчками.
6--Константы следует определять при помощи перечислений enum в большей степени, чем препроцессором. Так можно собрать константы из одной темы в одном месте и они не будут разбросаны по всему проекту.
7–Не вставлять функции внутрь if() . Коды возврата приходится анализировать пошаговым отладчиком до проверки условия.
это очень плохо:
Код:
if (MmGet(ID_IPv4_ROLE, tmp, 1, &tmp_len) != MM_RET_CODE_OK) {
return ERROR_CODE_HARDWARE_FAULT;
}
Код:
int ret = MmGet(ID_IPv4_ROLE, tmp, 1, &tmp_len);
if (ret != MM_RET_CODE_OK) {
return ERROR_CODE_HARDWARE_FAULT;
}
*9–Используй препроцессорный #error для предупреждения о нарушении зависимостей между компонентами.
Код:
#ifndef ADC_DRV_H
#define ADC_DRV_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stdint.h>
#include "adc_bsp.h"
#include "adc_types.h"
#ifndef HAS_MCU
#error "+ HAS_MCU"
#endif
#ifndef HAS_ADC
#error "+ HAS_ADC"
#endif
bool adc_init_channel(uint8_t adc_num, AdcChannel_t adc_channel);
bool adc_init(void);
bool adc_proc(void);
bool adc_channel_read(uint8_t adc_num, uint16_t adc_channel, uint32_t* code);
#ifdef __cplusplus
}
#endif
#endif /* ADC_DRV_H */
*10--Если что-то можно проверить на этапе make файлов, то это надо проверить на этапе make файлов. Каждый компонент должен проверять, что подключены нужные зависимости. Это можно сделать через условные операторы make файлов.
Код:
$(info I2S_MK_INC=$(I2S_MK_INC))
ifneq ($(I2S_MK_INC),Y)
I2S_MK_INC=Y
mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST)))
$(info Build $(mkfile_path) )
I2S_DIR = $(WORKSPACE_LOC)bsp/bsp_stm32f4/i2s
#@echo $(error I2S_DIR=$(I2S_DIR))
INCDIR += -I$(I2S_DIR)
OPT += -DHAS_I2S
SOURCES_C += $(I2S_DIR)/i2s_drv.c
ifeq ($(DIAG),Y)
SOURCES_C += $(I2S_DIR)/i2s_diag.c
endif
ifeq ($(CLI),Y)
ifeq ($(I2S_COMMANDS),Y)
OPT += -DHAS_I2S_COMMANDS
SOURCES_C += $(I2S_DIR)/i2s_commands.c
endif
endif
endif
*11--Если что-то можно проверить на этапе препроцессора, то это надо проверить на этапе препроцессора. Каждый компонент должен проверять, что подключены нужные зависимости. Это можно сделать через макросы компонентов.
*12–Если что-то можно проверить на этапе компиляции, то это надо проверить на этапе компиляции (static_assert(ы)). Например можно проверить, что в конфигурациях скорость UART не равна нулю. В RunTime не должно быть проверок, которые можно произвести на этапе компиляции, препроцессора или make файлов.
*13–Каждой set функции должна быть поставлена в соответствие get функция. И наоборот. Это позволит написать модульный тест для данного параметра.
*14–Если переменная это физическая величина, то в суффиксе указывать размерность (timeout_ms). Это увеличивает понятность кода.
*15–Все Си-функции должны всегда возвращать код ошибки. Минимум тип bool или числовой код ошибки. Так можно понять, где именно что-то пошло не так. Проще говоря, не должно быть функций, которые возвращают void. Функции void это, по факту, бомбы с часовым механизмом. В один день они отработают ошибочно, а вы об этом ничего даже не узнаете.
*16–Для каждого программного компонента создавать несколько *.с *.h файлов:
Файл компонента или драйвера | h | c |
файл констант | * | |
файл типов данных | * | |
файл команд CLI | * | * |
файл энергонезависимых параметров | * | |
файлы конфигурации по умолчанию | * | * |
файлы диагностики | * | * |
файлы с модульными тестами | * | * |
файлы самого драйвера. Функционал и бизнес логика. | * | * |
17–Если функция получает указатель, то пусть сразу проверяет на нуль значение указателя. Так прошивки не будут падать при получении нулевых указателей. Это повысит надежность кода. Вы же не знаете как и кто этот код будет испытывать. Хорошая функция всегда проверяет то, что ей дают.
18–Если есть конечный автомат, то добавить счетчик циклов. Так можно будет проверить, что автомат вообще вертится.
19–В идеале все переменные должны иметь разные имена. Так было бы очень удобно делать поиск по grep. Но тут надо искать компромисс с наглядностью.
20–У каждой функции должен быть только 1 return. Это позволит дописать какой-то функционал в конце, зная, что он точно вызовется.
21–Не использовать операторы >, >= Вместо них использовать <, <= просто поменяв местами аргументы там, где это нужно. Это позволит интуитивно проще анализировать логику по коду. Человеку еще со времен школьной математики понятнее, когда то, что слева - то меньше, а то, что справа - то больше. Так как ось X стрелкой показывала вправо. Особенно удобно при проверке переменной на принадлежность интервалу. Получается, что > и >= это вообще два бессмысленных оператора в языке С.
*22–В проекте обязательно должны быть модульные тесты. Тесты это просто функции, которые вызывают другие функции в run-time. Это позволит сделать безболезненную перестройку кода, когда архитектура начнет скрипеть. Тесты можно вызывать как до запуска приложения, так и по команде из UART- CLI.