Жизнь нещадно заставляет применять операционные системы (ОС) для микроконтроллеров. На рынке существует немерянное количество подобных систем. Разработчики операционных систем, соревнуясь друг с другом, пытаются максимально увеличить функциональность своих продуктов. Это зачастую приводит к увеличению «тяжеловесности» системы, а также значительно повышает «порог вхождения» для программиста, разрабатывающего программное обеспечение встраиваемых систем.
Чтобы не мучатся с выбором ОС для своих проектов, а также не затуманить сознание изучением чужого продукта, а также освоить технику написания встраиваемых приложений под операционные системы, а также разобраться, что это вообще такое, я решил написать свою ОСьку. Свое не пахнет.
Предлагаемая ОСька (именно ОСька, язык не поворачивается назвать ее ОС, а тем более ОСРВ) кооперативная со статическими задачами. Как было отмечено выше, я не являюсь сторонником использования ОС для микроконтроллеров, но еще больше я не являюсь сторонником использования вытесняющих операционных систем в микроконтроллерах. Вытесняющая многозадачность, по сравнению с кооперативной, это не только сложные процедуры переключения контекста, но и ресурсоемкая синхронизация потоков. Использование динамических задач также значительно утяжеляет операционную систему.
Операционная система разрабатывалась под процессор семейства Cortex-M0. При небольших изменениях, касающихся правил сохранения-восстановления контекста, ее можно использовать для других типов процессоров.
Исходный код
#ifndef __INTOROS_H
#define __INTOROS_H
//Операционная система IntorOS
//Константы
#define IntorOSMaxKolvoZadach (2) //максимальное количество задач (резервирование памяти)
#define IntorOSRazmerSteka (1024) //размер стека под все задачи [байты] (резервирование памяти)
#define IntorOSError (0) //Обработка ошибок 0-зависнуть !0- Сброс МК
//Функции
//инициализация задачи
//аргументы TaskPointer - указатель на задачу, точка входа
//аргументы Stek - размер стека задачи в байтах
void InitTask(void (*TaskPointer)(void), unsigned long Stek);
//запуск операционной системы
//аргумент номер стартовой задачи
void StartOS(unsigned long Num);
//передать управление операционной системе, усыпить поток
//аргумент ms - время в миллисекундах
void Sleep(unsigned long ms);
//завершение задачи
static inline void EndTask(void){while(1)Sleep(0xFFFFFFFF);}
//остановить задачу
//аргумент - номер задачи
void StopTask(unsigned long Num);
//запустить ранее остановленную задачу
//аргумент - номер задачи
void StartTask(unsigned long Num);
#endif
#define _INTOROS_C
#include "stm32l0xx.h"
#include "IntorOS.h"
//тип данных Параметры Задачи
typedef struct
{
unsigned long TaskSleep;//время до запуска задачи в мС
unsigned long* SP; //указатель стека задачи
}
Task_t;
unsigned long KolvoTask;//количество задач
unsigned long KolvoTaskStek;//использование стека
unsigned long TaskNum;//номер текущей исполняемой задачи
Task_t TaskList[IntorOSMaxKolvoZadach];//список задач
unsigned long TaskStek[IntorOSRazmerSteka/4];//резервирование пямяти под стеки задач
//инициализация задачи
//аргументы TaskPointer - указатель на задачу, точка входа
//аргументы Stek - размер стека задачи
void InitTask(void (*TaskPointer)(void), unsigned long Stek)
{
//инициализация параметров задачи
TaskList[KolvoTask].TaskSleep=0;//время через которое произойдет возврат управления в задачу
TaskList[KolvoTask].SP=&(TaskStek[IntorOSRazmerSteka/4-1-KolvoTaskStek]);//указатель стека задачи
//инициализация стека задачи
//записать в стек точку входа в задачу (регистр LR)
TaskList[KolvoTask].SP--;
(*(TaskList[KolvoTask].SP))=(unsigned long)(TaskPointer);
TaskList[KolvoTask].SP--;//записать в стек R4
TaskList[KolvoTask].SP--;//записать в стек R5
TaskList[KolvoTask].SP--;//записать в стек R6
TaskList[KolvoTask].SP--;//записать в стек R7
TaskList[KolvoTask].SP--;//записать в стек R8
TaskList[KolvoTask].SP--;//записать в стек R9
TaskList[KolvoTask].SP--;//записать в стек R10
TaskList[KolvoTask].SP--;//записать в стек R11
TaskList[KolvoTask].SP--;//записать в стек R12
KolvoTask++;//инкремент количества задач (для следующего вызова)
KolvoTaskStek=KolvoTaskStek+Stek/4;//инкремент использование стека
//Проверяем распределение стека
if(KolvoTaskStek>(IntorOSRazmerSteka/4))
#if IntorOSError==0
while(1);//если ошибка в указании размера стека - зависнуть
#else
NVIC_SystemReset();//если ошибка в указании размера стека - Сброс МК
#endif
return;
}
//запуск операционной системы
//аргумент номер стартовой задачи
void StartOS(unsigned long Num)
{
SysTick_Config(SystemCoreClock/1000);//запускаем таймер задержки вызова задач квант 1мС
TaskNum=Num;//номер стартовой задачи
//Деинициализация стека стартовой задачи
TaskList[TaskNum].SP++;//списать со стека R12
TaskList[TaskNum].SP++;//списать со стека R11
TaskList[TaskNum].SP++;//списать со стека R10
TaskList[TaskNum].SP++;//списать со стека R9
TaskList[TaskNum].SP++;//списать со стека R8
TaskList[TaskNum].SP++;//списать со стека R7
TaskList[TaskNum].SP++;//списать со стека R6
TaskList[TaskNum].SP++;//списать со стека R5
TaskList[TaskNum].SP++;//списать со стека R4
TaskList[TaskNum].SP++;//списать со стека LR
__set_SP((unsigned long)TaskList[TaskNum].SP);//установить указатель стека запускаемой задачи
(*((void (*)(void))(*(TaskList[TaskNum].SP-1))))();//передаем управление в задачу
//если произошло завершение задачи
#if IntorOSError==0
while(1);//зависнуть
#else
NVIC_SystemReset();//Сброс МК
#endif
}
//остановить задачу
//аргумент - номер задачи
void StopTask(unsigned long Num)
{
TaskList[Num].TaskSleep=0xFFFFFFFF;
return;
}
//запустить ранее остановленную задачу
//аргумент - номер задачи
void StartTask(unsigned long Num)
{
if((~(TaskList[Num].TaskSleep))==0)
{//если задача была остановлена, запустить
TaskList[Num].TaskSleep=0x00000000;
}
return;
}
//прерывание системного таймера
void SysTick_Handler(void);
void SysTick_Handler(void)
{
TimingDelay++;//инкремент переменной системного таймера
for(int i=0;i<KolvoTask;i++)
{//перебираем задачи
if(((TaskList[i].TaskSleep)!=0) && ((~(TaskList[i].TaskSleep))!=0))
{//если время до запуска не 0 и не 0xFFFFFFFF
(TaskList[i].TaskSleep)--;//уменьшаем время до запуска
}
}
return;
}
#define SHT_PROGBITS 0x1
EXTERN KolvoTask
EXTERN TaskList
EXTERN TaskNum
PUBLIC Sleep
SECTION `.text`:CODE:NOROOT(2)
THUMB
// 8 //передать управление операционной системе
// 9 //аргумент время в лимлисекундах
// 10 void Sleep(unsigned long ms)
Sleep:
// 11 {
// 12 //сохраняем контекст
// 13 __asm("PUSH {R4-R7,LR}");
PUSH {R4-R7,LR}
// 14 __asm("MOV R4,R8");
MOV R4,R8
// 15 __asm("MOV R5,R9");
MOV R5,R9
// 16 __asm("MOV R6,R10");
MOV R6,R10
// 17 __asm("MOV R7,R11");
MOV R7,R11
// 18 __asm("PUSH {R4-R7}");
PUSH {R4-R7}
// 19 __asm("MOV R4,R12");
MOV R4,R12
// 20 __asm("PUSH {R4}");
PUSH {R4}
// 21 TaskList[TaskNum].TaskSleep=ms;//сохраняем время через которое произойдет возврат управления
LDR R1,Sleep_0
LDR R2,Sleep_0+0x4
LDR R3,[R1, #+0]
LSLS R3,R3,#+3
STR R0,[R2, R3]
// 22 TaskList[TaskNum].SP =__get_SP();//сохраняем SP
MOV R0,SP
LDR R3,[R1, #+0]
LSLS R3,R3,#+3
ADDS R3,R2,R3
STR R0,[R3, #+4]
// 23 //выбор задачи для исполнения
// 24 while(1)
// 25 {
// 26 TaskNum++;if(TaskNum==KolvoTask)TaskNum=0;//инкрементируем номер текущей задачи
Sleep_1:
LDR R0,[R1, #+0]
ADDS R0,R0,#+1
LDR R3,Sleep_0+0x8
LDR R3,[R3, #+0]
CMP R0,R3
BNE Sleep_2
MOVS R0,#+0
Sleep_2:
STR R0,[R1, #+0]
LSLS R0,R0,#+3
ADDS R0,R2,R0
LDR R3,[R0, #+0]
CMP R3,#+0
BNE Sleep_1
// 27 //проверяем готовность задачи к выполнению
// 28 if(TaskList[TaskNum].TaskSleep==0)
// 29 {//задача готова к выполнению
// 30 //востанавливаем контекст
// 31 __set_SP(TaskList[TaskNum].SP);//востанавливаем SP
LDR R0,[R0, #+4]
MOV SP,R0
// 32 __asm("POP {R4}");
POP {R4}
// 33 __asm("MOV R12,R4");
MOV R12,R4
// 34 __asm("POP {R4-R7}");
POP {R4-R7}
// 35 __asm("MOV R11,R7");
MOV R11,R7
// 36 __asm("MOV R10,R6");
MOV R10,R6
// 37 __asm("MOV R9,R5");
MOV R9,R5
// 38 __asm("MOV R8,R4");
MOV R8,R4
// 39 __asm("POP {R4-R7,PC}");
POP {R4-R7,PC}
// 40
// 41 //The End
// 42 return;
NOP
// 43 }
// 44 }
// 45 }
DATA
Sleep_0:
// extern unsigned long TaskNum;//номер текущей задачи
DC32 TaskNum
// extern Task_t TaskList[IntorOSMaxKolvoZadach];//список задач
DC32 TaskList
// extern unsigned long KolvoTask;//количество задач
DC32 KolvoTask
SECTION `.iar_vfe_header`:DATA:NOALLOC:NOROOT(2)
SECTION_TYPE SHT_PROGBITS, 0
DATA
DC32 0
SECTION __DLIB_PERTHREAD:DATA:REORDER:NOROOT(0)
SECTION_TYPE SHT_PROGBITS, 0
SECTION __DLIB_PERTHREAD_init:DATA:REORDER:NOROOT(0)
SECTION_TYPE SHT_PROGBITS, 0
END
.cpu cortex-m0
.text
.cfi_sections .debug_frame
.section .text.Sleep,"ax",%progbits
.align 1
.global Sleep
.syntax unified
.thumb
.thumb_func
.type Sleep, %function
.extern KolvoTask
.extern TaskList
.extern TaskNum
.cfi_startproc
// 8 //передать управление операционной системе
// 9 //аргумент время в лимлисекундах
// 10 void Sleep(unsigned long ms)
Sleep:
// 11 {
// 12 //сохраняем контекст
// 13 __asm("PUSH {R4-R7,LR}");
PUSH {R4-R7,LR}
// 14 __asm("MOV R4,R8");
MOV R4,R8
// 15 __asm("MOV R5,R9");
MOV R5,R9
// 16 __asm("MOV R6,R10");
MOV R6,R10
// 17 __asm("MOV R7,R11");
MOV R7,R11
// 18 __asm("PUSH {R4-R7}");
PUSH {R4-R7}
// 19 __asm("MOV R4,R12");
MOV R4,R12
// 20 __asm("PUSH {R4}");
PUSH {R4}
// 21 TaskList[TaskNum].TaskSleep=ms;//сохраняем время через которое произойдет возврат управления
LDR R1,Sleep_0
LDR R2,Sleep_0+0x4
LDR R3,[R1, #+0]
LSLS R3,R3,#+3
STR R0,[R2, R3]
// 22 TaskList[TaskNum].SP =__get_SP();//сохраняем SP
MOV R0,SP
LDR R3,[R1, #+0]
LSLS R3,R3,#+3
ADDS R3,R2,R3
STR R0,[R3, #+4]
// 23 //выбор задачи для исполнения
// 24 while(1)
// 25 {
// 26 TaskNum++;if(TaskNum==KolvoTask)TaskNum=0;//инкрементируем номер текущей задачи
Sleep_1:
LDR R0,[R1, #+0]
ADDS R0,R0,#+1
LDR R3,Sleep_0+0x8
LDR R3,[R3, #+0]
CMP R0,R3
BNE Sleep_2
MOVS R0,#+0
Sleep_2:
STR R0,[R1, #+0]
LSLS R0,R0,#+3
ADDS R0,R2,R0
LDR R3,[R0, #+0]
CMP R3,#+0
BNE Sleep_1
// 27 //проверяем готовность задачи к выполнению
// 28 if(TaskList[TaskNum].TaskSleep==0)
// 29 {//задача готова к выполнению
// 30 //востанавливаем контекст
// 31 __set_SP(TaskList[TaskNum].SP);//востанавливаем SP
LDR R0,[R0, #+4]
MOV SP,R0
// 32 __asm("POP {R4}");
POP {R4}
// 33 __asm("MOV R12,R4");
MOV R12,R4
// 34 __asm("POP {R4-R7}");
POP {R4-R7}
// 35 __asm("MOV R11,R7");
MOV R11,R7
// 36 __asm("MOV R10,R6");
MOV R10,R6
// 37 __asm("MOV R9,R5");
MOV R9,R5
// 38 __asm("MOV R8,R4");
MOV R8,R4
// 39 __asm("POP {R4-R7,PC}");
POP {R4-R7,PC}
// 40
// 41 //The End
// 42 return;
NOP
// 43 }
// 44 }
// 45 }
.align 2
Sleep_0:
// extern unsigned long TaskNum;//номер текущей задачи
.word TaskNum
// extern Task_t TaskList[IntorOSMaxKolvoZadach];//список задач
.word TaskList
// extern unsigned long KolvoTask;//количество задач
.word KolvoTask
.cfi_endproc
Константы компиляции ОСьки
#define IntorOSMaxKolvoZadach (2) //максимальное количество задач (резервирование памяти)
#define IntorOSRazmerSteka (1024) //размер стека под все задачи [байты] (резервирование памяти)
По религиозным причинам я не могу использовать динамическое выделение памяти, поэтому объем требуемой памяти необходимо указывать на этапе компиляции.
Сервисы ОСьки
void InitTask(void (*TaskPointer)(void), unsigned long Stek);
Инициализация задачи. Задача оформляется в виде функции, указатель на функцию передается процедуре инициализации. При инициализации необходимо указать размер стека, выделяемый задаче. Порядок инициализации задач определяет их идентификаторы. Задача, инициализируемая первой, имеет идентификатор 0. Если указать суммарный размер стека больше зарезервированного произойдет ошибка. При инициализации задачи настраивается указатель стека задачи, стек загружается контекстом задачи.
void StartOS(unsigned long Num);
Старт операционной системы. В качестве аргумента функции передается идентификатор задачи, с которой необходимо начать выполнение. При старте операционной системы настраивается системный таймер на квант одна миллисекунда. Со стека запускаемой задачи списывается контекст и вызывается задача.
void Sleep(unsigned long ms);
Планировщик. При вызове этой функции из задачи, управление передается операционной системе. Операционная система из списка выбирает задачу готовую для исполнения и передает ей управление. Аргумент функции – время в миллисекундах, через которое необходимо вернуть управление текущей задаче. При вызове функции с аргументом 0xFFFFFFFF возврат управления не произойдет никогда.
Данную функцию невозможно написать на языке Си, так алгоритм ее работы полностью разрушает логику языка. В исходных кодах приведены тесты программ на языке ассемблера для систем программирования IAR и GCC. Для страждущих приведен код на языке Си. Но хотелось бы отметить, что правильно скомпилироваться он способен только при определенных «фазах луны». В моем случае это произошло только при использовании среднего уровня оптимизации, на низком и на высоком уровне код компилировался ошибочно.
extern Task_t TaskList[IntorOSMaxKolvoZadach];//список задач
extern unsigned long TaskNum;//номер текущей задачи
extern unsigned long KolvoTask;//количество задач
//передать управление операционной системе
//аргумент время в лимлисекундах
#pragma optimize=medium
void Sleep(unsigned long ms)
{
//сохраняем контекст
__asm("PUSH {R4-R7,LR}");
__asm("MOV R4,R8");
__asm("MOV R5,R9");
__asm("MOV R6,R10");
__asm("MOV R7,R11");
__asm("PUSH {R4-R7}");
__asm("MOV R4,R12");
__asm("PUSH {R4}");
TaskList[TaskNum].TaskSleep=ms;//сохраняем время через которое произойдет возврат управления
TaskList[TaskNum].SP =(unsigned long*)__get_SP();//сохраняем SP
//выбор задачи для исполнения
while(1)
{
TaskNum++;if(TaskNum==KolvoTask)TaskNum=0;//инкрементируем номер текущей задачи
//проверяем готовность задачи к выполнению
if(TaskList[TaskNum].TaskSleep==0)
{//задача готова к выполнению
//востанавливаем контекст
__set_SP((unsigned long)TaskList[TaskNum].SP);//востанавливаем SP
__asm("POP {R4}");
__asm("MOV R12,R4");
__asm("POP {R4-R7}");
__asm("MOV R11,R7");
__asm("MOV R10,R6");
__asm("MOV R9,R5");
__asm("MOV R8,R4");
__asm("POP {R4-R7,PC}"); //return
}
}
}
void EndTask(void);
Завершение задачи. Как отмечалось выше, задачи являются статическими, выгрузка задачи невозможна. Если необходимо задачу завершить можно воспользоваться этой функцией. Задача при этом остается в списке, но управление ей не передается.
void StopTask(unsigned long Num);
void StartTask(unsigned long Num);
Остановить или запустить задачу. Аргумент – идентификатор задачи. Эти функции позволяют реализовать диспетчер задач. Стоить отметить, что запустить можно только ранее остановленную задачу, время до запуска которой равно 0xFFFFFFFF.
Использование ОСьки
Для примера традиционный микроконтроллерный «хелворд» под разработанную операционную систему.
#include "stm32l0xx.h"
#include "stm32l0xx_ll_gpio.h"
#include "IntorOS.h"
//Задача 0
void Task0(void)
{
LL_GPIO_InitTypeDef GPIO_InitStruct;
LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOB);
GPIO_InitStruct.Pin = LL_GPIO_PIN_0;
GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
while(1)
{
GPIOB->BRR=LL_GPIO_PIN_0;
Sleep(1000);
GPIOB->BSRR=LL_GPIO_PIN_0;
Sleep(1000);
}
}
//Задача 1
void Task1(void)
{
LL_GPIO_InitTypeDef GPIO_InitStruct;
LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOB);
GPIO_InitStruct.Pin = LL_GPIO_PIN_1;
GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
while(1)
{
GPIOB->BRR=LL_GPIO_PIN_1;
Sleep(500);
GPIOB->BSRR=LL_GPIO_PIN_1;
Sleep(500);
}
}
void main(void)
{
// MCU Configuration
SystemClock_Config();
//Инициализация задач
InitTask(Task0, 512);
InitTask(Task1, 256);
//Запуск ОС
StartOS(0);
}
В заключении хочется искренне надеяться, что эта, по приколу, разработанная ОСька будет интересна и полезна разработчикам программного обеспечения для встраиваемых систем.
Комментарии (18)
jcmvbkbc
08.04.2019 23:06+2Данную функцию невозможно написать на языке Си, так алгоритм ее работы полностью разрушает логику языка.
Да ладно, на раз такая функция пишется на чистом С, без ассемблера и без фаз луны:
void Sleep(unsigned long ms) { TaskList[TaskNum].TaskSleep = ms; if (setjmp(TaskList[TaskNum].context) == 0) { while(1) { .... if(TaskList[TaskNum].TaskSleep==0) longjmp(TaskList[TaskNum].context, 1); ... } }
IBAH_II Автор
09.04.2019 11:10Не хиляет…
Как найти точку входа в задачу?jcmvbkbc
09.04.2019 12:17Как найти точку входа в задачу?
Есть варианты. Один — руками заполнить jmp_buf в InitTask. Для этого надо знать его структуру. Другой — в InitTask сделать что-то такое:
static void InitTask1(jmp_buf *buf, void (*TaskPointer)(void), unsigned long stack) { set_stack_pointer(stack); // установить указатель стека созданной задачи if (setjmp(TaskList[TaskNum].context) == 0) { longjmp(*buf, 1); } else { TaskPointer(); KillTask(); } } void InitTask(void (*TaskPointer)(void), unsigned long stack) { jmp_buf buf; if (setjmp(buf) == 0) { InitTask1(&buf, TaskPointer, stack); } }
Возможно лучше сделать это через третью промежуточную функцию на ассемблере, которая только и делает, что устанавливает указатель стека.IBAH_II Автор
10.04.2019 14:20Возможно лучше сделать это через третью промежуточную функцию на ассемблере, которая только и делает, что устанавливает указатель стека.
Что я и делаю без этих хлопот…jcmvbkbc
10.04.2019 20:29Что я и делаю без этих хлопот…
Я не сомневаюсь. Мой комментарий был оДанную функцию невозможно написать на языке Си
.
poulch
08.04.2019 23:33лет 10 назад для AVR была похожая оська YAVRTOS. сейчас сайт ее мертв, но архивы все еще находятся гуглом. в свое время оказалась очень полезной для меня к изучению…
IBAH_II Автор
09.04.2019 11:15YAVRTOS? спасибо, посмотрю
Коментарии обнадеживающие
forum.cxem.net/index.php?/blogs/entry/541-%D0%BA%D0%B0%D0%BA-%D0%BF%D0%B5%D1%80%D0%B5%D1%81%D1%82%D0%B0%D1%82%D1%8C-%D0%B2%D0%BE%D0%BB%D0%BD%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%D1%81%D1%8F-%D0%B8-%D0%BF%D0%BE%D0%BB%D1%8E%D0%B1%D0%B8%D1%82%D1%8C-rtospoulch
09.04.2019 20:08в веб архиве лучше наверное искать. те ссылки что находят сейчас — не последняя версия. Если нужно, то могу в почту прислать сохраненную в свое время последнюю версию yavrtos (version 1.11, 2009 Apr 02)
Polaris99
09.04.2019 14:09Мне кажется, что для большинства задач, для решения которых, собственно, и стоит использовать ОСРВ, необходима вытесняющая многозадачность. Кооперативная ОСРВ со статическими задачами ничего особо не дает в сравнении даже с банальной машиной состояний. Наверное, поэтому большинство кооперативных ОСРВ и умерли давно и безвозвратно.
IBAH_II Автор
09.04.2019 17:27Мне ОС нужна для возможности использования чужих исходников, написанных в блокирующем стиле.
lamerok
В качестве тренировки написания своего это хорошо!
Но еще раз убеждаюсь, что электронщик и программист в микроконтроллерах это должны быть два разных человека.
Вот проще не бывает, порт для кооперативного super simple tasker. И проще и линтом проверен и кода меньше.
github.com/QuantumLeaps/qpc/tree/master/ports/arm-cm/qv/iar
www.state-machine.com/qpc/api.html#api_qv
Koyanisqatsi
Сильное заявление. И справедливое только для большого и сложного проекта, но 90% (цифра из головы) проектов не такие, поэтому лучше и денежней совмещать.
nightmare-1966
Да уж, сказано сильно — только вот работодатели про это обычно не знают :-)…
Помнится, ходил на собеседку в «Электроприбор», хорошую для Питера зарплату предлагали, вполне на уровне для «Инженер, регулировщик РЭА».
В результате выяснилось, что они от работника хотят и программирование АРМ на С++, и для ПЛИС Ксайлинкс/Альтера на Верилог чтоб ошибки находил в коде…
Ну чтобы минимум за пару квалифицированных работников работал за одну зарплату.
Вот такие «скромные» хотелки у работодателей нередко встречаются.
lamerok
Мое мнение, что хороший продукт при этом получится может с вероятностью 50 на 50. Может получится, а может нет. Если делается поделка в двух экземплярах, то да так можно делать. Но если, делается электроника и софт для производства 10000 приборов в год. То боже упаси совмещать две эти роли. Один баг в софте или ошибка проектирования электроники и компания теряет 1 000 000 долларов при цене прибора 100 баксов. И кого вы думаете назначаи козлом отпущения?
I_Soba
Не могу полностью согласится, я считаю, что тут дело в опыте и квалификации, и всех грести под одну гребенку я бы не стал. Если условный «Эмбеддер», к примеру, может понимать ассемблер для отлова багов, знать си для средних и мелких проектов, а также уметь в с++ для крупных (то бишь разрабатываемыми приличной командой), знает особенности архитектуры контроллера и именно встраиваемого программирования, понимает принципы RTOS(по необходимости), разбирается как в схемотехнике, так и в корректной трассировке плат, то почему бы и не совмещать эти роли. Тогда, это совсем не обязательно будет мелкая поделка. Очевидно, вышеописанный специалист всегда найдет себе работу с адекватными требованиями и соответствующей оплатой труда.